diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aaf7c6269e..6c2c23868c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,22 +22,23 @@ on: env: CacheFolders: | common - deploy + desktop + desktop-package dev models packages plugins pods - products server server-plugins templates + services tests qms-tests rush.json .prettierrc tools - + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" @@ -47,7 +48,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 0 filter: tree:0 - uses: actions/setup-node@v4 with: @@ -74,7 +75,7 @@ jobs: - name: Model version from git tags run: node common/scripts/install-run-rush.js model-version - + - name: Building... run: node common/scripts/install-run-rush.js build -p 20 @@ -114,6 +115,7 @@ jobs: path: ${{ env.CacheFolders}} key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ github.sha }} restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-${{ github.sha }} + - name: Checking svelte sources... run: node common/scripts/install-run-rush.js svelte-check formatting: @@ -144,8 +146,8 @@ jobs: echo '================================================================' echo 'Checking for diff files' echo '================================================================' - git diff '*.js' '*.ts' '*.svelte' '*.json' | cat - [ -z "$(git diff --name-only '*.js' '*.ts' '*.svelte' '*.json' | cat)" ] + git diff '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat + [ -z "$(git diff --name-only '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat)" ] echo '================================================================' - name: Formatting... run: node common/scripts/install-run-rush.js fast-format @@ -154,8 +156,8 @@ jobs: echo '================================================================' echo 'Checking for diff files' echo '================================================================' - git diff '*.js' '*.ts' '*.svelte' '*.json' | cat - [ -z "$(git diff --name-only '*.js' '*.ts' '*.svelte' '*.json' | cat)" ] + git diff '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat + [ -z "$(git diff --name-only '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat)" ] echo '================================================================' test: needs: build @@ -208,13 +210,13 @@ jobs: key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }} - + - name: Checking for mis-matching dependencies... run: node common/scripts/install-run-rush.js check - + - name: Installing... run: node common/scripts/install-run-rush.js install - + - name: Docker Build run: node common/scripts/install-run-rush.js docker:build -p 20 env: @@ -222,7 +224,7 @@ jobs: - name: Prepare server run: | cd ./tests - export DO_CLEAN=true + export DO_CLEAN="true" ./prepare.sh - name: Install Playwright run: | @@ -232,7 +234,7 @@ jobs: run: | cd ./tests/sanity node ../../common/scripts/install-run-rushx.js uitest - - name: "Store docker logs" + - name: 'Store docker logs' if: always() run: | cd ./tests/sanity @@ -308,13 +310,13 @@ jobs: key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }} - + - name: Checking for mis-matching dependencies... run: node common/scripts/install-run-rush.js check - + - name: Installing... run: node common/scripts/install-run-rush.js install - + - name: Docker Build run: node common/scripts/install-run-rush.js docker:build -p 20 env: @@ -332,7 +334,7 @@ jobs: run: | cd ./qms-tests/sanity node ../../common/scripts/install-run-rushx.js uitest - - name: "Store docker logs" + - name: 'Store docker logs' if: always() run: | cd ./qms-tests/sanity @@ -402,23 +404,23 @@ jobs: key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }} - + - name: Checking for mis-matching dependencies... run: node common/scripts/install-run-rush.js check - + - name: Installing... run: node common/scripts/install-run-rush.js install - + - name: Docker Build run: node common/scripts/install-run-rush.js docker:build -p 20 env: DOCKER_CLI_HINTS: false - name: Prepare server - env: + env: SERVER_PROVIDER: uweb run: | cd ./tests - ./prepare.sh + ./prepare.sh - name: Install Playwright run: | cd ./tests/sanity @@ -427,7 +429,7 @@ jobs: run: | cd ./tests/sanity node ../../common/scripts/install-run-rushx.js uitest - - name: "Store docker logs" + - name: 'Store docker logs' if: always() run: | cd ./tests/sanity @@ -452,8 +454,8 @@ jobs: # uses: actions/upload-artifact@v3 # with: # name: db-snapshot - # path: ./tests/db_dump - docker-build: + # path: ./tests/db_dump + docker-build: needs: [build, test, svelte-check, uitest] runs-on: ubuntu-latest timeout-minutes: 60 @@ -475,15 +477,15 @@ jobs: key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }} - + - name: Installing... run: node common/scripts/install-run-rush.js install - name: Model version from git tags run: node common/scripts/install-run-rush.js model-version - + - name: Docker build - run: node common/scripts/install-run-rush.js docker:build -p 20 + run: node common/scripts/install-run-rush.js docker:build -v env: DOCKER_CLI_HINTS: false - name: Login to Docker Hub @@ -494,9 +496,112 @@ jobs: password: ${{ secrets.DOCKER_ACCESS_TOKEN }} - name: Docker push staging if: ${{ github.ref == 'refs/heads/main' }} - run: node common/scripts/install-run-rush.js docker:staging + run: node common/scripts/install-run-rush.js docker:staging -v - name: Docker push tag if: ${{ startsWith(github.ref, 'refs/tags/v') }} run: | echo Pushing release of tag ${{ github.ref }} - node common/scripts/install-run-rush.js docker:push + node common/scripts/install-run-rush.js docker:push -v + dist-build: + # if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + needs: build + runs-on: macos-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + filter: tree:0 + submodules: recursive + + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - name: Cache node modules + uses: actions/cache@v4 + env: + cache-name: node + with: + path: | + common/temp + key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Installing... + run: node common/scripts/install-run-rush.js install --purge + - name: Model version from git tags + run: node common/scripts/install-run-rush.js model-version + - name: Package + run: node common/scripts/install-run-rush.js package --to desktop -p 20 -v + - name: Package JSON + run: | + cd desktop-package + cat ./package.json + - name: Install the Apple certificate and provisioning profile + env: + DEV_ID_P12_BASE64: ${{ secrets.DEV_ID_P12_BASE64 }} + DEV_ID_P12_PASSWORD: ${{ secrets.DEV_ID_P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + # create variables + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + # import certificate from secret + echo -n "$DEV_ID_P12_BASE64" | base64 --decode -o $CERTIFICATE_PATH + + # create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$DEV_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + - name: Build distribution's + env: + AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + run: | + cd desktop-package + node ../common/scripts/install-run-rushx.js dist --linux --windows --x64 + - name: Build distribution's + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_APP_PASS: ${{ secrets.APPLE_ID_APP_PASS }} + TEAM_ID: ${{ secrets.TEAM_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + run: | + cd desktop-package + node ../common/scripts/install-run-rushx.js dist-signed --macos --x64 --arm64 + - name: Publish distribution version + env: + AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + run: | + ./desktop-package/scripts/publish-version.sh + - name: Upload MacOS + uses: actions/upload-artifact@v3 + with: + name: Huly-MacOS-x64 + path: ./desktop-package/deploy/Huly-macos-*-x64.dmg + - name: Upload MacOS arm64 + uses: actions/upload-artifact@v3 + with: + name: Huly-MacOS-arm64 + path: ./desktop-package/deploy/Huly-macos-*-arm64.dmg + - name: Upload Windows + uses: actions/upload-artifact@v3 + with: + name: Huly-Windows + path: ./desktop-package/deploy/Huly-windows-*.zip + - name: Upload Linux + uses: actions/upload-artifact@v3 + with: + name: Huly-Linux + path: ./desktop-package/deploy/Huly-linux-*.zip diff --git a/.gitignore b/.gitignore index b203d1c095..5d39fa5430 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,11 @@ tests/db_dump .build .format tools/apm/apm.js +deploy +metrics.txt +services/github/pod-github/src/github.graphql +.build +.format +dev/tool/report.csv +bundle/* +bundle.js.map diff --git a/common/config/rush/command-line.json b/common/config/rush/command-line.json index ba6b86cd30..11fc7155cc 100644 --- a/common/config/rush/command-line.json +++ b/common/config/rush/command-line.json @@ -49,6 +49,14 @@ "ignoreMissingScript": true, "allowWarningsOnSuccess": false }, + { + "name": "_phase:format", + "dependencies": { + "self": ["_phase:build"] + }, + "ignoreMissingScript": true, + "allowWarningsOnSuccess": false + }, { "name": "_phase:svelte-check", "dependencies": { @@ -81,10 +89,21 @@ "self": ["_phase:build", "_phase:package", "_phase:bundle"] }, "ignoreMissingScript": true, - "allowWarningsOnSuccess": false + "allowWarningsOnSuccess": true } ], - "commands": [ + "commands": [ + { + "commandKind": "bulk", + "name": "format", + "summary": "Format", + "description": "Perform a formatting", + "enableParallelism": true, + "incremental": false, + "ignoreMissingScript": true, + "safeForSimultaneousRushProcesses": true, + "disableBuildCache": true + }, { "commandKind": "phased", "name": "build:watch", @@ -99,17 +118,6 @@ "watchPhases": ["_phase:build", "_phase:validate"] } }, - { - "commandKind": "bulk", - "name": "format", - "summary": "Format", - "description": "Perform a formatting", - "enableParallelism": true, - "incremental": false, - "ignoreMissingScript": true, - "safeForSimultaneousRushProcesses": true, - "disableBuildCache": true - }, { "summary": "svelte-check", "commandKind": "phased", @@ -124,7 +132,11 @@ "summary": "build", "phases": ["_phase:build"], "enableParallelism": true, - "incremental": true + "incremental": true, + "watchOptions": { + "alwaysWatch": false, + "watchPhases": ["_phase:build"] + } }, { "commandKind": "phased", @@ -193,12 +205,12 @@ "incremental": true }, { - "commandKind": "global", - "name": "docker:up", - "summary": "Up development build", - "description": "Up development build", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "cd ./dev && docker compose up -d --force-recreate" + "summary": "docker:rebuild", + "commandKind": "phased", + "name": "docker:rebuild", + "phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-build"], + "enableParallelism": true, + "incremental": false }, { "summary": "docker:staging", @@ -220,6 +232,30 @@ "disableBuildCache": true, "allowWarningsInSuccessfulBuild": true }, + { + "commandKind": "global", + "name": "docker", + "summary": "Build docker with platform", + "description": "use to build all docker containers required for platform", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "rush docker:build -p 20 --to @hcengineering/pod-server --to @hcengineering/pod-front --to @hcengineering/prod --to @hcengineering/pod-account --to @hcengineering/pod-collaborator --to @hcengineering/tool --to @hcengineering/pod-print --to @hcengineering/pod-sign --to @hcengineering/pod-analytics-collector --to @hcengineering/rekoni-service" + }, + { + "commandKind": "global", + "name": "docker:up", + "summary": "Up development build", + "description": "Up development build", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "cd ./dev && docker compose up -d --force-recreate" + }, + { + "commandKind": "global", + "name": "tool:upgrade", + "summary": "Upgrade all local models", + "description": "Upgrade all local models", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "cd ./dev/tool && rushx run-local upgrade -f" + }, { "commandKind": "global", "name": "apply-templates", diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index c31c0213a7..f8b0b7e4a8 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: '@elastic/elasticsearch': specifier: ^7.14.0 version: 7.17.13 + '@electron/notarize': + specifier: ^2.3.2 + version: 2.3.2 '@faker-js/faker': specifier: ^8.4.1 version: 8.4.1 @@ -41,6 +44,15 @@ dependencies: '@livekit/track-processors': specifier: ~0.3.1 version: 0.3.2(livekit-client@2.2.0) + '@octokit/types': + specifier: ^12.0.0 + version: 12.6.0 + '@octokit/webhooks': + specifier: ^12.0.3 + version: 12.2.0 + '@octokit/webhooks-types': + specifier: ^7.3.1 + version: 7.5.1 '@playwright/test': specifier: ^1.41.2 version: 1.41.2 @@ -62,6 +74,15 @@ dependencies: '@rush-temp/analytics': specifier: file:./projects/analytics.tgz version: file:projects/analytics.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/analytics-collector': + specifier: file:./projects/analytics-collector.tgz + version: file:projects/analytics-collector.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2) + '@rush-temp/analytics-collector-assets': + specifier: file:./projects/analytics-collector-assets.tgz + version: file:projects/analytics-collector-assets.tgz(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2) + '@rush-temp/analytics-service': + specifier: file:./projects/analytics-service.tgz + version: file:projects/analytics-service.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) '@rush-temp/apm': specifier: file:./projects/apm.tgz version: file:projects/apm.tgz(ts-node@10.9.2) @@ -152,12 +173,36 @@ dependencies: '@rush-temp/core': specifier: file:./projects/core.tgz version: file:projects/core.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/desktop': + specifier: file:./projects/desktop.tgz + version: file:projects/desktop.tgz(bufferutil@4.0.8)(sass@1.71.1)(utf-8-validate@6.0.4) + '@rush-temp/desktop-1': + specifier: file:./projects/desktop-1.tgz + version: file:projects/desktop-1.tgz(webpack@5.90.3) + '@rush-temp/desktop-preferences': + specifier: file:./projects/desktop-preferences.tgz + version: file:projects/desktop-preferences.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/desktop-preferences-assets': + specifier: file:./projects/desktop-preferences-assets.tgz + version: file:projects/desktop-preferences-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/desktop-preferences-resources': + specifier: file:./projects/desktop-preferences-resources.tgz + version: file:projects/desktop-preferences-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2) '@rush-temp/devmodel': specifier: file:./projects/devmodel.tgz version: file:projects/devmodel.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) '@rush-temp/devmodel-resources': specifier: file:./projects/devmodel-resources.tgz version: file:projects/devmodel-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2) + '@rush-temp/diffview': + specifier: file:./projects/diffview.tgz + version: file:projects/diffview.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/diffview-assets': + specifier: file:./projects/diffview-assets.tgz + version: file:projects/diffview-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/diffview-resources': + specifier: file:./projects/diffview-resources.tgz + version: file:projects/diffview-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2) '@rush-temp/document': specifier: file:./projects/document.tgz version: file:projects/document.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) @@ -182,6 +227,15 @@ dependencies: '@rush-temp/front': specifier: file:./projects/front.tgz version: file:projects/front.tgz(esbuild@0.20.1) + '@rush-temp/github': + specifier: file:./projects/github.tgz + version: file:projects/github.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/github-assets': + specifier: file:./projects/github-assets.tgz + version: file:projects/github-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/github-resources': + specifier: file:./projects/github-resources.tgz + version: file:projects/github-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2) '@rush-temp/gmail': specifier: file:./projects/gmail.tgz version: file:projects/gmail.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) @@ -269,6 +323,9 @@ dependencies: '@rush-temp/model-all': specifier: file:./projects/model-all.tgz version: file:projects/model-all.tgz + '@rush-temp/model-analytics-collector': + specifier: file:./projects/model-analytics-collector.tgz + version: file:projects/model-analytics-collector.tgz '@rush-temp/model-attachment': specifier: file:./projects/model-attachment.tgz version: file:projects/model-attachment.tgz @@ -293,12 +350,18 @@ dependencies: '@rush-temp/model-core': specifier: file:./projects/model-core.tgz version: file:projects/model-core.tgz + '@rush-temp/model-desktop-preferences': + specifier: file:./projects/model-desktop-preferences.tgz + version: file:projects/model-desktop-preferences.tgz '@rush-temp/model-document': specifier: file:./projects/model-document.tgz version: file:projects/model-document.tgz '@rush-temp/model-drive': specifier: file:./projects/model-drive.tgz version: file:projects/model-drive.tgz + '@rush-temp/model-github': + specifier: file:./projects/model-github.tgz + version: file:projects/model-github.tgz '@rush-temp/model-gmail': specifier: file:./projects/model-gmail.tgz version: file:projects/model-gmail.tgz @@ -454,7 +517,7 @@ dependencies: version: file:projects/model-templates.tgz '@rush-temp/model-text-editor': specifier: file:./projects/model-text-editor.tgz - version: file:projects/model-text-editor.tgz(@tiptap/pm@2.2.4) + version: file:projects/model-text-editor.tgz '@rush-temp/model-time': specifier: file:./projects/model-time.tgz version: file:projects/model-time.tgz @@ -500,18 +563,45 @@ dependencies: '@rush-temp/pod-account': specifier: file:./projects/pod-account.tgz version: file:projects/pod-account.tgz + '@rush-temp/pod-analytics-collector': + specifier: file:./projects/pod-analytics-collector.tgz + version: file:projects/pod-analytics-collector.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@rush-temp/pod-backup': specifier: file:./projects/pod-backup.tgz version: file:projects/pod-backup.tgz + '@rush-temp/pod-calendar': + specifier: file:./projects/pod-calendar.tgz + version: file:projects/pod-calendar.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4) '@rush-temp/pod-collaborator': specifier: file:./projects/pod-collaborator.tgz version: file:projects/pod-collaborator.tgz '@rush-temp/pod-front': specifier: file:./projects/pod-front.tgz version: file:projects/pod-front.tgz + '@rush-temp/pod-github': + specifier: file:./projects/pod-github.tgz + version: file:projects/pod-github.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-prosemirror@1.2.2) + '@rush-temp/pod-gmail': + specifier: file:./projects/pod-gmail.tgz + version: file:projects/pod-gmail.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4) + '@rush-temp/pod-love': + specifier: file:./projects/pod-love.tgz + version: file:projects/pod-love.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@rush-temp/pod-print': + specifier: file:./projects/pod-print.tgz + version: file:projects/pod-print.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@rush-temp/pod-server': specifier: file:./projects/pod-server.tgz version: file:projects/pod-server.tgz + '@rush-temp/pod-ses': + specifier: file:./projects/pod-ses.tgz + version: file:projects/pod-ses.tgz + '@rush-temp/pod-sign': + specifier: file:./projects/pod-sign.tgz + version: file:projects/pod-sign.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@rush-temp/pod-telegram': + specifier: file:./projects/pod-telegram.tgz + version: file:projects/pod-telegram.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4) '@rush-temp/preference': specifier: file:./projects/preference.tgz version: file:projects/preference.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) @@ -542,6 +632,9 @@ dependencies: '@rush-temp/products-resources': specifier: file:./projects/products-resources.tgz version: file:projects/products-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2) + '@rush-temp/qms-doc-import-tool': + specifier: file:./projects/qms-doc-import-tool.tgz + version: file:projects/qms-doc-import-tool.tgz '@rush-temp/qms-tests-sanity': specifier: file:./projects/qms-tests-sanity.tgz version: file:projects/qms-tests-sanity.tgz @@ -572,6 +665,9 @@ dependencies: '@rush-temp/rekoni': specifier: file:./projects/rekoni.tgz version: file:projects/rekoni.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/rekoni-service': + specifier: file:./projects/rekoni-service.tgz + version: file:projects/rekoni-service.tgz(webpack@5.90.3) '@rush-temp/request': specifier: file:./projects/request.tgz version: file:projects/request.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) @@ -596,6 +692,12 @@ dependencies: '@rush-temp/server-activity-resources': specifier: file:./projects/server-activity-resources.tgz version: file:projects/server-activity-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/server-analytics-collector': + specifier: file:./projects/server-analytics-collector.tgz + version: file:projects/server-analytics-collector.tgz(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/server-analytics-collector-resources': + specifier: file:./projects/server-analytics-collector-resources.tgz + version: file:projects/server-analytics-collector-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) '@rush-temp/server-attachment': specifier: file:./projects/server-attachment.tgz version: file:projects/server-attachment.tgz(esbuild@0.20.1)(ts-node@10.9.2) @@ -653,6 +755,15 @@ dependencies: '@rush-temp/server-drive-resources': specifier: file:./projects/server-drive-resources.tgz version: file:projects/server-drive-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/server-github': + specifier: file:./projects/server-github.tgz + version: file:projects/server-github.tgz(esbuild@0.20.1)(ts-node@10.9.2) + '@rush-temp/server-github-model': + specifier: file:./projects/server-github-model.tgz + version: file:projects/server-github-model.tgz + '@rush-temp/server-github-resources': + specifier: file:./projects/server-github-resources.tgz + version: file:projects/server-github-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) '@rush-temp/server-gmail': specifier: file:./projects/server-gmail.tgz version: file:projects/server-gmail.tgz(esbuild@0.20.1)(ts-node@10.9.2) @@ -923,6 +1034,24 @@ dependencies: '@rush-temp/workbench-resources': specifier: file:./projects/workbench-resources.tgz version: file:projects/workbench-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2) + '@sentry/node': + specifier: ^7.103.0 + version: 7.118.0 + '@sentry/svelte': + specifier: ~7.101.0 + version: 7.101.1(svelte@4.2.12) + '@signpdf/placeholder-pdf-lib': + specifier: ^3.2.4 + version: 3.2.4(pdf-lib@1.17.1) + '@signpdf/signer-p12': + specifier: ^3.2.4 + version: 3.2.4(node-forge@1.3.1) + '@signpdf/signpdf': + specifier: ^3.2.4 + version: 3.2.4 + '@signpdf/utils': + specifier: ^3.2.4 + version: 3.2.4 '@storybook/addon-essentials': specifier: ^7.0.6 version: 7.6.17(react-dom@18.2.0)(react@18.2.0) @@ -1028,6 +1157,9 @@ dependencies: '@tiptap/suggestion': specifier: ^2.2.4 version: 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tsconfig/node16': + specifier: ^1.0.4 + version: 1.0.4 '@types/body-parser': specifier: ~1.19.2 version: 1.19.5 @@ -1043,18 +1175,33 @@ dependencies: '@types/diff': specifier: ~5.0.2 version: 5.0.9 + '@types/domhandler': + specifier: ^2.4.5 + version: 2.4.5 + '@types/email-addresses': + specifier: ^3.0.0 + version: 3.0.0 '@types/express': specifier: ^4.17.13 version: 4.17.21 '@types/express-fileupload': specifier: ^1.1.7 version: 1.4.4 + '@types/express-form-data': + specifier: ^2.0.5 + version: 2.0.5 '@types/html-to-text': specifier: ^8.1.1 version: 8.1.1 + '@types/htmlparser2': + specifier: ^3.10.7 + version: 3.10.7 '@types/jest': specifier: ^29.5.5 version: 29.5.12 + '@types/jimp': + specifier: ^0.2.28 + version: 0.2.28 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -1079,6 +1226,9 @@ dependencies: '@types/markdown-it': specifier: ~13.0.0 version: 13.0.8 + '@types/mime': + specifier: ^3.0.1 + version: 3.0.4 '@types/mime-types': specifier: ~2.1.1 version: 2.1.4 @@ -1100,6 +1250,9 @@ dependencies: '@types/passport-google-oauth20': specifier: ^2.0.0 version: 2.0.14 + '@types/pdfjs-dist': + specifier: 2.10.378 + version: 2.10.378 '@types/png-chunks-extract': specifier: ^1.0.2 version: 1.0.2 @@ -1160,6 +1313,9 @@ dependencies: '@uppy/xhr-upload': specifier: ^3.6.8 version: 3.6.8(@uppy/core@3.13.1) + '@vercel/webpack-asset-relocator-loader': + specifier: ^1.7.3 + version: 1.7.4 allure-playwright: specifier: ^2.9.2 version: 2.12.2 @@ -1169,9 +1325,15 @@ dependencies: autoprefixer: specifier: ^10.4.14 version: 10.4.17(postcss@8.4.35) + aws-sdk: + specifier: ^2.1423.0 + version: 2.1664.0 base64-js: specifier: ^1.5.1 version: 1.5.1 + big-integer: + specifier: ^1.6.51 + version: 1.6.52 body-parser: specifier: ^1.20.2 version: 1.20.2 @@ -1193,6 +1355,9 @@ dependencies: compression-webpack-plugin: specifier: ^10.0.0 version: 10.0.0(webpack@5.90.3) + copy-webpack-plugin: + specifier: ^11.0.0 + version: 11.0.0(webpack@5.90.3) cors: specifier: ^2.8.5 version: 2.8.5 @@ -1223,6 +1388,15 @@ dependencies: diff: specifier: ^5.1.0 version: 5.2.0 + diff2html: + specifier: ~3.4.35 + version: 3.4.48 + domhandler: + specifier: ^5.0.3 + version: 5.0.3 + domutils: + specifier: ^3.1.0 + version: 3.1.0 dotenv: specifier: ~16.0.0 version: 16.0.3 @@ -1235,6 +1409,30 @@ dependencies: elastic-apm-node: specifier: ~3.26.0 version: 3.26.0 + electron: + specifier: ^31.3.0 + version: 31.3.0 + electron-builder: + specifier: ^24.13.3 + version: 24.13.3 + electron-context-menu: + specifier: ^4.0.1 + version: 4.0.1 + electron-log: + specifier: ^5.1.7 + version: 5.1.7 + electron-squirrel-startup: + specifier: ~1.0.0 + version: 1.0.1 + electron-store: + specifier: ^10.0.0 + version: 10.0.0 + electron-updater: + specifier: ^6.2.1 + version: 6.2.1 + electron-windows-badge: + specifier: ^1.1.0 + version: 1.1.0 email-addresses: specifier: ^5.0.0 version: 5.0.0 @@ -1268,6 +1466,9 @@ dependencies: eslint-plugin-n: specifier: ^15.4.0 version: 15.7.0(eslint@8.56.0) + eslint-plugin-node: + specifier: ^11.1.0 + version: 11.1.0(eslint@8.56.0) eslint-plugin-promise: specifier: ^6.1.1 version: 6.1.1(eslint@8.56.0) @@ -1280,6 +1481,9 @@ dependencies: express-fileupload: specifier: ^1.5.1 version: 1.5.1 + express-form-data: + specifier: ^2.0.22 + version: 2.0.23 express-static-gzip: specifier: ^2.1.7 version: 2.1.7 @@ -1289,6 +1493,9 @@ dependencies: fast-equals: specifier: ^5.0.1 version: 5.0.1 + file-api: + specifier: ^0.10.4 + version: 0.10.4 file-loader: specifier: ^6.2.0 version: 6.2.0(webpack@5.90.3) @@ -1298,15 +1505,39 @@ dependencies: fork-ts-checker-webpack-plugin: specifier: ~7.3.0 version: 7.3.0(typescript@5.3.3)(webpack@5.90.3) + form-data: + specifier: ^4.0.0 + version: 4.0.0 + gaxios: + specifier: ^5.0.1 + version: 5.1.3 + google-auth-library: + specifier: ^8.0.2 + version: 8.9.0 + googleapis: + specifier: ^122.0.0 + version: 122.0.0 got: specifier: ^11.8.3 version: 11.8.6 + graphql: + specifier: ^16.8.0 + version: 16.9.0 + graphql-binding: + specifier: ^2.5.2 + version: 2.5.2 + highlight.js: + specifier: ~11.8.0 + version: 11.8.0 html-to-text: specifier: ^9.0.3 version: 9.0.5 html-webpack-plugin: specifier: ^5.5.0 version: 5.6.0(webpack@5.90.3) + htmlparser2: + specifier: ^9.0.0 + version: 9.1.0 intl-messageformat: specifier: ^9.7.1 version: 9.13.0 @@ -1316,6 +1547,9 @@ dependencies: jest-environment-jsdom: specifier: 29.7.0 version: 29.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + jimp: + specifier: ^0.16.1 + version: 0.16.13 js-yaml: specifier: ^4.1.0 version: 4.1.0 @@ -1349,9 +1583,18 @@ dependencies: livekit-client: specifier: ^2.0.10 version: 2.2.0 + livekit-server-sdk: + specifier: ^2.0.10 + version: 2.6.0 + mammoth: + specifier: ^1.6.0 + version: 1.8.0 markdown-it: specifier: ^14.0.0 version: 14.0.0 + mime: + specifier: ^3.0.0 + version: 3.0.0 mime-types: specifier: ~2.1.34 version: 2.1.35 @@ -1373,6 +1616,15 @@ dependencies: node-fetch: specifier: ^2.6.6 version: 2.7.0 + node-forge: + specifier: ^1.3.1 + version: 1.3.1 + node-loader: + specifier: ~2.0.0 + version: 2.0.0(webpack@5.90.3) + octokit: + specifier: ^3.1.1 + version: 3.2.1 p-queue: specifier: ~7.3.0 version: 7.3.4 @@ -1382,6 +1634,12 @@ dependencies: passport-google-oauth20: specifier: ~2.0.0 version: 2.0.0 + pdf-lib: + specifier: ^1.12.0 + version: 1.17.1 + pdfjs-dist: + specifier: 2.12.313 + version: 2.12.313 png-chunks-extract: specifier: ^1.0.0 version: 1.0.0 @@ -1394,6 +1652,9 @@ dependencies: postcss-loader: specifier: ^7.0.2 version: 7.3.4(postcss@8.4.35)(typescript@5.3.3)(webpack@5.90.3) + posthog-js: + specifier: ~1.122.0 + version: 1.122.0 prettier: specifier: ^3.1.0 version: 3.2.5 @@ -1406,6 +1667,9 @@ dependencies: prosemirror-model: specifier: ^1.19.4 version: 1.19.4 + puppeteer: + specifier: ^22.6.1 + version: 22.14.0(bufferutil@4.0.8)(typescript@5.3.3)(utf-8-validate@6.0.4) qs: specifier: ~6.11.0 version: 6.11.2 @@ -1439,6 +1703,9 @@ dependencies: smartcrop: specifier: ~2.0.5 version: 2.0.5 + smee-client: + specifier: ^1.2.3 + version: 1.2.5 storybook: specifier: ^7.0.6 version: 7.6.17(bufferutil@4.0.8)(utf-8-validate@6.0.4) @@ -1469,6 +1736,9 @@ dependencies: tar-stream: specifier: ^2.2.0 version: 2.2.0 + telegram: + specifier: 2.22.2 + version: 2.22.2 toposort: specifier: ^2.0.2 version: 2.0.2 @@ -1481,6 +1751,12 @@ dependencies: ts-node: specifier: ^10.8.0 version: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.19)(typescript@5.3.3) + ts-standard: + specifier: ^12.0.2 + version: 12.0.2(typescript@5.3.3) typescript: specifier: ^5.3.3 version: 5.3.3 @@ -1511,6 +1787,12 @@ dependencies: webpack-dev-server: specifier: ^4.11.1 version: 4.15.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)(webpack-cli@5.1.4)(webpack@5.90.3) + winston: + specifier: ^3.11.0 + version: 3.13.1 + winston-daily-rotate-file: + specifier: ^5.0.0 + version: 5.0.0(winston@3.13.1) ws: specifier: ^8.18.0 version: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) @@ -1523,9 +1805,16 @@ dependencies: yjs: specifier: ^13.5.52 version: 13.6.12 + zod: + specifier: ^3.22.4 + version: 3.23.8 packages: + /7zip-bin@5.2.0: + resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==} + dev: false + /@aashutoshrathi/word-wrap@1.2.6: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} @@ -3529,6 +3818,15 @@ packages: dev: false optional: true + /@colors/colors@1.6.0: + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + dev: false + + /@cryptography/aes@0.1.1: + resolution: {integrity: sha512-PcYz4FDGblO6tM2kSC+VzhhK62vml6k6/YAkiWtyPvrgJVfnDRoHGDtKn5UiaRRUrvUTTocBpvc2rRgTCqxjsg==} + dev: false + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -3536,6 +3834,22 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: false + /@dabh/diagnostics@2.0.3: + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + dev: false + + /@develar/schema-utils@2.6.5: + resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} + engines: {node: '>= 8.9.0'} + dependencies: + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: false + /@discoveryjs/json-ext@0.5.7: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -3565,6 +3879,85 @@ packages: - supports-color dev: false + /@electron/asar@3.2.10: + resolution: {integrity: sha512-mvBSwIBUeiRscrCeJE1LwctAriBj65eUDm0Pc11iE5gRwzkmsdbS7FnZ1XUWjpSeQWL1L5g12Fc/SchPM9DUOw==} + engines: {node: '>=10.12.0'} + hasBin: true + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.2 + dev: false + + /@electron/get@2.0.3: + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + dependencies: + debug: 4.3.5 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@electron/notarize@2.2.1: + resolution: {integrity: sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==} + engines: {node: '>= 10.0.0'} + dependencies: + debug: 4.3.4 + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@electron/notarize@2.3.2: + resolution: {integrity: sha512-zfayxCe19euNwRycCty1C7lF7snk9YwfRpB5M8GLr1a4ICH63znxaPNAubrMvj0yDvVozqfgsdYpXVUnpWBDpg==} + engines: {node: '>= 10.0.0'} + dependencies: + debug: 4.3.4 + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@electron/osx-sign@1.0.5: + resolution: {integrity: sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + compare-version: 0.1.2 + debug: 4.3.4 + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@electron/universal@1.5.1: + resolution: {integrity: sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==} + engines: {node: '>=8.6'} + dependencies: + '@electron/asar': 3.2.10 + '@malept/cross-spawn-promise': 1.1.1 + debug: 4.3.4 + dir-compare: 3.3.0 + fs-extra: 9.1.0 + minimatch: 3.1.2 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + dev: false + /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0): resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} peerDependencies: @@ -4654,6 +5047,389 @@ packages: chalk: 4.1.2 dev: false + /@jimp/bmp@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-9edAxu7N2FX7vzkdl5Jo1BbACfycUtBQX+XBMcHA2bk62P8R0otgkHg798frgAk/WxQIzwxqOH6wMiCwrlAzdQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + bmp-js: 0.1.0 + dev: false + + /@jimp/core@0.16.13: + resolution: {integrity: sha512-qXpA1tzTnlkTku9yqtuRtS/wVntvE6f3m3GNxdTdtmc+O+Wcg9Xo2ABPMh7Nc0AHbMKzwvwgB2JnjZmlmJEObg==} + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/utils': 0.16.13 + any-base: 1.1.0 + buffer: 5.7.1 + exif-parser: 0.1.12 + file-type: 16.5.4 + load-bmfont: 1.4.2 + mkdirp: 0.5.6 + phin: 2.9.3 + pixelmatch: 4.0.2 + tinycolor2: 1.6.0 + transitivePeerDependencies: + - debug + dev: false + + /@jimp/custom@0.16.13: + resolution: {integrity: sha512-LTATglVUPGkPf15zX1wTMlZ0+AU7cGEGF6ekVF1crA8eHUWsGjrYTB+Ht4E3HTrCok8weQG+K01rJndCp/l4XA==} + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/core': 0.16.13 + transitivePeerDependencies: + - debug + dev: false + + /@jimp/gif@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-yFAMZGv3o+YcjXilMWWwS/bv1iSqykFahFMSO169uVMtfQVfa90kt4/kDwrXNR6Q9i6VHpFiGZMlF2UnHClBvg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + gifwrap: 0.9.4 + omggif: 1.0.10 + dev: false + + /@jimp/jpeg@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-BJHlDxzTlCqP2ThqP8J0eDrbBfod7npWCbJAcfkKqdQuFk0zBPaZ6KKaQKyKxmWJ87Z6ohANZoMKEbtvrwz1AA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + jpeg-js: 0.4.4 + dev: false + + /@jimp/plugin-blit@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-8Z1k96ZFxlhK2bgrY1JNWNwvaBeI/bciLM0yDOni2+aZwfIIiC7Y6PeWHTAvjHNjphz+XCt01WQmOYWCn0ML6g==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-blur@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-PvLrfa8vkej3qinlebyhLpksJgCF5aiysDMSVhOZqwH5nQLLtDE9WYbnsofGw4r0VVpyw3H/ANCIzYTyCtP9Cg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-circle@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-RNave7EFgZrb5V5EpdvJGAEHMnDAJuwv05hKscNfIYxf0kR3KhViBTDy+MoTnMlIvaKFULfwIgaZWzyhuINMzA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-color@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-xW+9BtEvoIkkH/Wde9ql4nAFbYLkVINhpgAE7VcBUsuuB34WUbcBl/taOuUYQrPEFQJ4jfXiAJZ2H/rvKjCVnQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + tinycolor2: 1.6.0 + dev: false + + /@jimp/plugin-contain@0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-blit@0.16.13)(@jimp/plugin-resize@0.16.13)(@jimp/plugin-scale@0.16.13): + resolution: {integrity: sha512-QayTXw4tXMwU6q6acNTQrTTFTXpNRBe+MgTGMDU0lk+23PjlFCO/9sacflelG8lsp7vNHhAxFeHptDMAksEYzg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blit': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + '@jimp/plugin-scale': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/plugin-blit': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-resize': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-scale': 0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-resize@0.16.13) + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-cover@0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-crop@0.16.13)(@jimp/plugin-resize@0.16.13)(@jimp/plugin-scale@0.16.13): + resolution: {integrity: sha512-BSsP71GTNaqWRcvkbWuIVH+zK7b3TSNebbhDkFK0fVaUTzHuKMS/mgY4hDZIEVt7Rf5FjadAYtsujHN9w0iSYA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-crop': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + '@jimp/plugin-scale': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/plugin-crop': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-resize': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-scale': 0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-resize@0.16.13) + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-crop@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-WEl2tPVYwzYL8OKme6Go2xqiWgKsgxlMwyHabdAU4tXaRwOCnOI7v4021gCcBb9zn/oWwguHuKHmK30Fw2Z/PA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-displace@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-qt9WKq8vWrcjySa9DyQ0x/RBMHQeiVjdVSY1SJsMjssPUf0pS74qorcuAkGi89biN3YoGUgPkpqECnAWnYwgGA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-dither@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-5/N3yJggbWQTlGZHQYJPmQXEwR52qaXjEzkp1yRBbtdaekXE3BG/suo0fqeoV/csf8ooI78sJzYmIrxNoWVtgQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-fisheye@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-2rZmTdFbT/cF9lEZIkXCYO0TsT114Q27AX5IAo0Sju6jVQbvIk1dFUTnwLDadTo8wkJlFzGqMQ24Cs8cHWOliA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-flip@0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-rotate@0.16.13): + resolution: {integrity: sha512-EmcgAA74FTc5u7Z+hUO/sRjWwfPPLuOQP5O64x5g4j0T12Bd29IgsYZxoutZo/rb3579+JNa/3wsSEmyVv1EpA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-rotate': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/plugin-rotate': 0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-blit@0.16.13)(@jimp/plugin-crop@0.16.13)(@jimp/plugin-resize@0.16.13) + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-gaussian@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-A1XKfGQD0iDdIiKqFYi8nZMv4dDVYdxbrmgR7y/CzUHhSYdcmoljLIIsZZM3Iks/Wa353W3vtvkWLuDbQbch1w==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-invert@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-xFMrIn7czEZbdbMzZWuaZFnlLGJDVJ82y5vlsKsXRTG2kcxRsMPXvZRWHV57nSs1YFsNqXSbrC8B98n0E32njQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-mask@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-wLRYKVBXql2GAYgt6FkTnCfE+q5NomM7Dlh0oIPGAoMBWDyTx0eYutRK6PlUrRK2yMHuroAJCglICTbxqGzowQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-normalize@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-3tfad0n9soRna4IfW9NzQdQ2Z3ijkmo21DREHbE6CGcMIxOSvfRdSvf1qQPApxjTSo8LTU4MCi/fidx/NZ0GqQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-print@0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-blit@0.16.13): + resolution: {integrity: sha512-0m6i3p01PGRkGAK9r53hDYrkyMq+tlhLOIbsSTmZyh6HLshUKlTB7eXskF5OpVd5ZUHoltlNc6R+ggvKIzxRFw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blit': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/plugin-blit': 0.16.13(@jimp/custom@0.16.13) + '@jimp/utils': 0.16.13 + load-bmfont: 1.4.2 + transitivePeerDependencies: + - debug + dev: false + + /@jimp/plugin-resize@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-qoqtN8LDknm3fJm9nuPygJv30O3vGhSBD2TxrsCnhtOsxKAqVPJtFVdGd/qVuZ8nqQANQmTlfqTiK9mVWQ7MiQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-rotate@0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-blit@0.16.13)(@jimp/plugin-crop@0.16.13)(@jimp/plugin-resize@0.16.13): + resolution: {integrity: sha512-Ev+Jjmj1nHYw897z9C3R9dYsPv7S2/nxdgfFb/h8hOwK0Ovd1k/+yYS46A0uj/JCKK0pQk8wOslYBkPwdnLorw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blit': '>=0.3.5' + '@jimp/plugin-crop': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/plugin-blit': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-crop': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-resize': 0.16.13(@jimp/custom@0.16.13) + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-scale@0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-resize@0.16.13): + resolution: {integrity: sha512-05POQaEJVucjTiSGMoH68ZiELc7QqpIpuQlZ2JBbhCV+WCbPFUBcGSmE7w4Jd0E2GvCho/NoMODLwgcVGQA97A==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/plugin-resize': 0.16.13(@jimp/custom@0.16.13) + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-shadow@0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-blur@0.16.13)(@jimp/plugin-resize@0.16.13): + resolution: {integrity: sha512-nmu5VSZ9hsB1JchTKhnnCY+paRBnwzSyK5fhkhtQHHoFD5ArBQ/5wU8y6tCr7k/GQhhGq1OrixsECeMjPoc8Zw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blur': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/plugin-blur': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-resize': 0.16.13(@jimp/custom@0.16.13) + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugin-threshold@0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-color@0.16.13)(@jimp/plugin-resize@0.16.13): + resolution: {integrity: sha512-+3zArBH0OE3Rhjm4HyAokMsZlIq5gpQec33CncyoSwxtRBM2WAhUVmCUKuBo+Lr/2/4ISoY4BWpHKhMLDix6cA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-color': '>=0.8.0' + '@jimp/plugin-resize': '>=0.8.0' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/plugin-color': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-resize': 0.16.13(@jimp/custom@0.16.13) + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/plugins@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-CJLdqODEhEVs4MgWCxpWL5l95sCBlkuSLz65cxEm56X5akIsn4LOlwnKoSEZioYcZUBvHhCheH67AyPTudfnQQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/plugin-blit': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-blur': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-circle': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-color': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-contain': 0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-blit@0.16.13)(@jimp/plugin-resize@0.16.13)(@jimp/plugin-scale@0.16.13) + '@jimp/plugin-cover': 0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-crop@0.16.13)(@jimp/plugin-resize@0.16.13)(@jimp/plugin-scale@0.16.13) + '@jimp/plugin-crop': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-displace': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-dither': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-fisheye': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-flip': 0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-rotate@0.16.13) + '@jimp/plugin-gaussian': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-invert': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-mask': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-normalize': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-print': 0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-blit@0.16.13) + '@jimp/plugin-resize': 0.16.13(@jimp/custom@0.16.13) + '@jimp/plugin-rotate': 0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-blit@0.16.13)(@jimp/plugin-crop@0.16.13)(@jimp/plugin-resize@0.16.13) + '@jimp/plugin-scale': 0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-resize@0.16.13) + '@jimp/plugin-shadow': 0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-blur@0.16.13)(@jimp/plugin-resize@0.16.13) + '@jimp/plugin-threshold': 0.16.13(@jimp/custom@0.16.13)(@jimp/plugin-color@0.16.13)(@jimp/plugin-resize@0.16.13) + timm: 1.7.1 + transitivePeerDependencies: + - debug + dev: false + + /@jimp/png@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-8cGqINvbWJf1G0Her9zbq9I80roEX0A+U45xFby3tDWfzn+Zz8XKDF1Nv9VUwVx0N3zpcG1RPs9hfheG4Cq2kg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + pngjs: 3.4.0 + dev: false + + /@jimp/tiff@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-oJY8d9u95SwW00VPHuCNxPap6Q1+E/xM5QThb9Hu+P6EGuu6lIeLaNBMmFZyblwFbwrH+WBOZlvIzDhi4Dm/6Q==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + utif: 2.0.1 + dev: false + + /@jimp/types@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-mC0yVNUobFDjoYLg4hoUwzMKgNlxynzwt3cDXzumGvRJ7Kb8qQGOWJQjQFo5OxmGExqzPphkirdbBF88RVLBCg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/bmp': 0.16.13(@jimp/custom@0.16.13) + '@jimp/custom': 0.16.13 + '@jimp/gif': 0.16.13(@jimp/custom@0.16.13) + '@jimp/jpeg': 0.16.13(@jimp/custom@0.16.13) + '@jimp/png': 0.16.13(@jimp/custom@0.16.13) + '@jimp/tiff': 0.16.13(@jimp/custom@0.16.13) + timm: 1.7.1 + dev: false + + /@jimp/utils@0.16.13: + resolution: {integrity: sha512-VyCpkZzFTHXtKgVO35iKN0sYR10psGpV6SkcSeV4oF7eSYlR8Bl6aQLCzVeFjvESF7mxTmIiI3/XrMobVrtxDA==} + dependencies: + '@babel/runtime': 7.23.9 + regenerator-runtime: 0.13.11 + dev: false + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -4731,6 +5507,12 @@ packages: '@bufbuild/protobuf': 1.10.0 dev: false + /@livekit/protocol@1.19.1: + resolution: {integrity: sha512-PQYIuqRv++fRik9tKulJ0C0tT5O4cNviBA7OxwLTCBFDxJpve8ua8/JZ+nK+7r4j2KbLfVjsJYop9wcTCgRn7Q==} + dependencies: + '@bufbuild/protobuf': 1.10.0 + dev: false + /@livekit/track-processors@0.3.2(livekit-client@2.2.0): resolution: {integrity: sha512-4JUCzb7yIKoVsTo8J6FTzLZJHcI6DihfX/pGRDg0SOGaxprcDPrt8jaDBBTsnGBSXHeMxl2ugN+xQjdCWzLKEA==} peerDependencies: @@ -4741,6 +5523,25 @@ packages: livekit-client: 2.2.0 dev: false + /@malept/cross-spawn-promise@1.1.1: + resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==} + engines: {node: '>= 10'} + dependencies: + cross-spawn: 7.0.3 + dev: false + + /@malept/flatpak-bundler@0.4.0: + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + dependencies: + debug: 4.3.4 + fs-extra: 9.1.0 + lodash: 4.17.21 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + dev: false + /@mdx-js/react@2.3.0(react@18.2.0): resolution: {integrity: sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==} peerDependencies: @@ -4848,6 +5649,278 @@ packages: fastq: 1.17.1 dev: false + /@octokit/app@14.1.0: + resolution: {integrity: sha512-g3uEsGOQCBl1+W1rgfwoRFUIR6PtvB2T1E4RpygeUU5LrLvlOqcxrt5lfykIeRpUPpupreGJUYl70fqMDXdTpw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-app': 6.1.1 + '@octokit/auth-unauthenticated': 5.0.1 + '@octokit/core': 5.2.0 + '@octokit/oauth-app': 6.1.0 + '@octokit/plugin-paginate-rest': 9.2.1(@octokit/core@5.2.0) + '@octokit/types': 12.6.0 + '@octokit/webhooks': 12.2.0 + dev: false + + /@octokit/auth-app@6.1.1: + resolution: {integrity: sha512-VrTtzRpyuT5nYGUWeGWQqH//hqEZDV+/yb6+w5wmWpmmUA1Tx950XsAc2mBBfvusfcdF2E7w8jZ1r1WwvfZ9pA==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-app': 7.1.0 + '@octokit/auth-oauth-user': 4.1.0 + '@octokit/request': 8.4.0 + '@octokit/request-error': 5.1.0 + '@octokit/types': 13.5.0 + deprecation: 2.3.1 + lru-cache: 10.2.0 + universal-github-app-jwt: 1.1.2 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-oauth-app@7.1.0: + resolution: {integrity: sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-device': 6.1.0 + '@octokit/auth-oauth-user': 4.1.0 + '@octokit/request': 8.4.0 + '@octokit/types': 13.5.0 + '@types/btoa-lite': 1.0.2 + btoa-lite: 1.0.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-oauth-device@6.1.0: + resolution: {integrity: sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/oauth-methods': 4.1.0 + '@octokit/request': 8.4.0 + '@octokit/types': 13.5.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-oauth-user@4.1.0: + resolution: {integrity: sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-device': 6.1.0 + '@octokit/oauth-methods': 4.1.0 + '@octokit/request': 8.4.0 + '@octokit/types': 13.5.0 + btoa-lite: 1.0.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-token@4.0.0: + resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} + engines: {node: '>= 18'} + dev: false + + /@octokit/auth-unauthenticated@5.0.1: + resolution: {integrity: sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request-error': 5.1.0 + '@octokit/types': 12.6.0 + dev: false + + /@octokit/core@5.2.0: + resolution: {integrity: sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-token': 4.0.0 + '@octokit/graphql': 7.1.0 + '@octokit/request': 8.4.0 + '@octokit/request-error': 5.1.0 + '@octokit/types': 13.5.0 + before-after-hook: 2.2.3 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/endpoint@9.0.5: + resolution: {integrity: sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/types': 13.5.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/graphql@7.1.0: + resolution: {integrity: sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request': 8.4.0 + '@octokit/types': 13.5.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/oauth-app@6.1.0: + resolution: {integrity: sha512-nIn/8eUJ/BKUVzxUXd5vpzl1rwaVxMyYbQkNZjHrF7Vk/yu98/YDF/N2KeWO7uZ0g3b5EyiFXFkZI8rJ+DH1/g==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-app': 7.1.0 + '@octokit/auth-oauth-user': 4.1.0 + '@octokit/auth-unauthenticated': 5.0.1 + '@octokit/core': 5.2.0 + '@octokit/oauth-authorization-url': 6.0.2 + '@octokit/oauth-methods': 4.1.0 + '@types/aws-lambda': 8.10.142 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/oauth-authorization-url@6.0.2: + resolution: {integrity: sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==} + engines: {node: '>= 18'} + dev: false + + /@octokit/oauth-methods@4.1.0: + resolution: {integrity: sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/oauth-authorization-url': 6.0.2 + '@octokit/request': 8.4.0 + '@octokit/request-error': 5.1.0 + '@octokit/types': 13.5.0 + btoa-lite: 1.0.0 + dev: false + + /@octokit/openapi-types@20.0.0: + resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==} + dev: false + + /@octokit/openapi-types@22.2.0: + resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==} + dev: false + + /@octokit/plugin-paginate-graphql@4.0.1(@octokit/core@5.2.0): + resolution: {integrity: sha512-R8ZQNmrIKKpHWC6V2gum4x9LG2qF1RxRjo27gjQcG3j+vf2tLsEfE7I/wRWEPzYMaenr1M+qDAtNcwZve1ce1A==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=5' + dependencies: + '@octokit/core': 5.2.0 + dev: false + + /@octokit/plugin-paginate-rest@11.3.1(@octokit/core@5.2.0): + resolution: {integrity: sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '5' + dependencies: + '@octokit/core': 5.2.0 + '@octokit/types': 13.5.0 + dev: false + + /@octokit/plugin-paginate-rest@9.2.1(@octokit/core@5.2.0): + resolution: {integrity: sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '5' + dependencies: + '@octokit/core': 5.2.0 + '@octokit/types': 12.6.0 + dev: false + + /@octokit/plugin-rest-endpoint-methods@13.2.2(@octokit/core@5.2.0): + resolution: {integrity: sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': ^5 + dependencies: + '@octokit/core': 5.2.0 + '@octokit/types': 13.5.0 + dev: false + + /@octokit/plugin-retry@6.0.1(@octokit/core@5.2.0): + resolution: {integrity: sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=5' + dependencies: + '@octokit/core': 5.2.0 + '@octokit/request-error': 5.1.0 + '@octokit/types': 12.6.0 + bottleneck: 2.19.5 + dev: false + + /@octokit/plugin-throttling@8.2.0(@octokit/core@5.2.0): + resolution: {integrity: sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': ^5.0.0 + dependencies: + '@octokit/core': 5.2.0 + '@octokit/types': 12.6.0 + bottleneck: 2.19.5 + dev: false + + /@octokit/request-error@5.1.0: + resolution: {integrity: sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==} + engines: {node: '>= 18'} + dependencies: + '@octokit/types': 13.5.0 + deprecation: 2.3.1 + once: 1.4.0 + dev: false + + /@octokit/request@8.4.0: + resolution: {integrity: sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/endpoint': 9.0.5 + '@octokit/request-error': 5.1.0 + '@octokit/types': 13.5.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/types@12.6.0: + resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==} + dependencies: + '@octokit/openapi-types': 20.0.0 + dev: false + + /@octokit/types@13.5.0: + resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==} + dependencies: + '@octokit/openapi-types': 22.2.0 + dev: false + + /@octokit/webhooks-methods@4.1.0: + resolution: {integrity: sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==} + engines: {node: '>= 18'} + dev: false + + /@octokit/webhooks-types@7.4.0: + resolution: {integrity: sha512-FE2V+QZ2UYlh+9wWd5BPLNXG+J/XUD/PPq0ovS+nCcGX4+3qVbi3jYOmCTW48hg9SBBLtInx9+o7fFt4H5iP0Q==} + dev: false + + /@octokit/webhooks-types@7.5.1: + resolution: {integrity: sha512-1dozxWEP8lKGbtEu7HkRbK1F/nIPuJXNfT0gd96y6d3LcHZTtRtlf8xz3nicSJfesADxJyDh+mWBOsdLkqgzYw==} + dev: false + + /@octokit/webhooks@12.2.0: + resolution: {integrity: sha512-CyuLJ0/P7bKZ+kIYw+fnkeVdhUzNuDKgNSI7pU/m7Nod0T7kP+s4s2f0pNmG9HL8/RZN1S0ZWTDll3VTMrFLAw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request-error': 5.1.0 + '@octokit/webhooks-methods': 4.1.0 + '@octokit/webhooks-types': 7.4.0 + aggregate-error: 3.1.0 + dev: false + + /@pdf-lib/standard-fonts@1.0.0: + resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==} + dependencies: + pako: 1.0.11 + dev: false + + /@pdf-lib/upng@1.0.1: + resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==} + dependencies: + pako: 1.0.11 + dev: false + /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -4871,6 +5944,23 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false + /@puppeteer/browsers@2.3.0: + resolution: {integrity: sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==} + engines: {node: '>=18'} + hasBin: true + dependencies: + debug: 4.3.5 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.4.0 + semver: 7.6.3 + tar-fs: 3.0.6 + unbzip2-stream: 1.4.3 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + dev: false + /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: @@ -5425,6 +6515,173 @@ packages: selderee: 0.11.0 dev: false + /@sentry-internal/feedback@7.101.1: + resolution: {integrity: sha512-fOKDMVvLX+FuJHJszKBvRg1m7+fd4hchqRnZ9DDfitT6P5Ppl0gbEt/LStqu8Wq5M0tna+hpdwHlVEt7gZVKzw==} + engines: {node: '>=12'} + dependencies: + '@sentry/core': 7.101.1 + '@sentry/types': 7.101.1 + '@sentry/utils': 7.101.1 + dev: false + + /@sentry-internal/replay-canvas@7.101.1: + resolution: {integrity: sha512-09l6nD+lxWvwkpXLlIZuzj/z79Llbo6mcH33TJvxrUTjAqSGF/i3Pd5bTLWro9atippOyQgIV/yTGG4Bc5FhyQ==} + engines: {node: '>=12'} + dependencies: + '@sentry/core': 7.101.1 + '@sentry/replay': 7.101.1 + '@sentry/types': 7.101.1 + '@sentry/utils': 7.101.1 + dev: false + + /@sentry-internal/tracing@7.101.1: + resolution: {integrity: sha512-ihjWG8x4x0ozx6t+EHoXLKbsPrgzYLCpeBLWyS+M6n3hn6cmHM76c8nZw3ldhUQi5UYL3LFC/JZ50b4oSxtlrg==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.101.1 + '@sentry/types': 7.101.1 + '@sentry/utils': 7.101.1 + dev: false + + /@sentry-internal/tracing@7.118.0: + resolution: {integrity: sha512-dERAshKlQLrBscHSarhHyUeGsu652bDTUN1FK0m4e3X48M3I5/s+0N880Qjpe5MprNLcINlaIgdQ9jkisvxjfw==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.118.0 + '@sentry/types': 7.118.0 + '@sentry/utils': 7.118.0 + dev: false + + /@sentry/browser@7.101.1: + resolution: {integrity: sha512-+rIFoWPdO29AHVYsAwq8QEl2Ihv17Xh9Bt2aPFvLTGDA0caHjnx98g2jSOvLIOah6HI7Nwp3Njg2zBEzDtHkNw==} + engines: {node: '>=8'} + dependencies: + '@sentry-internal/feedback': 7.101.1 + '@sentry-internal/replay-canvas': 7.101.1 + '@sentry-internal/tracing': 7.101.1 + '@sentry/core': 7.101.1 + '@sentry/replay': 7.101.1 + '@sentry/types': 7.101.1 + '@sentry/utils': 7.101.1 + dev: false + + /@sentry/core@7.101.1: + resolution: {integrity: sha512-XSmXXeYT1d4O14eDF3OXPJFUgaN2qYEeIGUztqPX9nBs9/ij8y/kZOayFqlIMnfGvjOUM+63sy/2xDBOpFn6ug==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.101.1 + '@sentry/utils': 7.101.1 + dev: false + + /@sentry/core@7.118.0: + resolution: {integrity: sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.118.0 + '@sentry/utils': 7.118.0 + dev: false + + /@sentry/integrations@7.118.0: + resolution: {integrity: sha512-C2rR4NvIMjokF8jP5qzSf1o2zxDx7IeYnr8u15Kb2+HdZtX559owALR0hfgwnfeElqMhGlJBaKUWZ48lXJMzCQ==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.118.0 + '@sentry/types': 7.118.0 + '@sentry/utils': 7.118.0 + localforage: 1.10.0 + dev: false + + /@sentry/node@7.118.0: + resolution: {integrity: sha512-79N63DvYKkNPqzmc0cjO+vMZ/nU7+CbE3K3COQNiV7gk58+666G9mRZQJuZVOVebatq5wM5UR0G4LPkwD+J84g==} + engines: {node: '>=8'} + dependencies: + '@sentry-internal/tracing': 7.118.0 + '@sentry/core': 7.118.0 + '@sentry/integrations': 7.118.0 + '@sentry/types': 7.118.0 + '@sentry/utils': 7.118.0 + dev: false + + /@sentry/replay@7.101.1: + resolution: {integrity: sha512-l4jmj2Rf/myzk3TA83PdMiomassG8okdBh1b2Hp1+ycBRVZFDmsR81gKPvnefSXwGwGNGKEmp6Q2bdGzekpp3Q==} + engines: {node: '>=12'} + dependencies: + '@sentry-internal/tracing': 7.101.1 + '@sentry/core': 7.101.1 + '@sentry/types': 7.101.1 + '@sentry/utils': 7.101.1 + dev: false + + /@sentry/svelte@7.101.1(svelte@4.2.12): + resolution: {integrity: sha512-2ZqBY6vgYAilvcNa8xkhGCr0vTSwWAtRD3WSeYoKt8SzGoPgarsDsYb64His10LPLO6DXBbuBQbe2Ku/57g71w==} + engines: {node: '>=8'} + peerDependencies: + svelte: 3.x || 4.x + dependencies: + '@sentry/browser': 7.101.1 + '@sentry/core': 7.101.1 + '@sentry/types': 7.101.1 + '@sentry/utils': 7.101.1 + magic-string: 0.30.7 + svelte: 4.2.12 + dev: false + + /@sentry/types@7.101.1: + resolution: {integrity: sha512-bwtkQvrCZ6JGc7vqX7TEAKBgkbQFORt84FFS3JQQb8G3efTt9fZd2ReY4buteKQdlALl8h1QWVngTLmI+kyUuw==} + engines: {node: '>=8'} + dev: false + + /@sentry/types@7.118.0: + resolution: {integrity: sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==} + engines: {node: '>=8'} + dev: false + + /@sentry/utils@7.101.1: + resolution: {integrity: sha512-Nrg0nrEI3nrOCd9SLJ/WGzxS5KMQE4cryLOvrDcHJRWpsSyGBF1hLLerk84Nsw/0myMsn7zTYU+xoq7idNsX5A==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.101.1 + dev: false + + /@sentry/utils@7.118.0: + resolution: {integrity: sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.118.0 + dev: false + + /@signpdf/placeholder-pdf-lib@3.2.4(pdf-lib@1.17.1): + resolution: {integrity: sha512-pNYzvLPWiHPir86pDWOK015S0whjNVvItvlTo9kwk1+99l2luIrwmubEvPjxSmDAWQ67UpKxA5lEYQsbZ7Q/og==} + engines: {node: '>=12', yarn: '>=1.22.18'} + peerDependencies: + pdf-lib: ^1.17.1 + dependencies: + '@signpdf/utils': 3.2.4 + pdf-lib: 1.17.1 + dev: false + + /@signpdf/signer-p12@3.2.4(node-forge@1.3.1): + resolution: {integrity: sha512-DTeYMj1DI30VYmaZXp2zZEg9YwVm3NmqjLF3q6ZxUwlOUSLLOUFkVafIs4YibTAGBmgjWKEMfmNCndK2ACLlzQ==} + engines: {node: '>=12', yarn: '>=1.22.18'} + peerDependencies: + node-forge: ^1.2.1 + dependencies: + '@signpdf/utils': 3.2.4 + node-forge: 1.3.1 + dev: false + + /@signpdf/signpdf@3.2.4: + resolution: {integrity: sha512-X/LlvUNjw2V6fsHej42LZ/DouyEXHcm4S4se53CAiUE+CDUlPrHlKIZOkGuzwl8SKuhFFFusIAs/75ZEN+5pQw==} + engines: {node: '>=12'} + dependencies: + '@signpdf/utils': 3.2.4 + dev: false + + /@signpdf/utils@3.2.4: + resolution: {integrity: sha512-6e7OZugRY6obyTbAKdj6yCIywCJqN0FDywsIrhkRnOTAKm4J3WVXq18NjzmE6iEXLknxok9EXNW/eVvY9Vp1gg==} + engines: {node: '>=12', yarn: '>=1.22.18'} + dev: false + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: false @@ -7384,11 +8641,19 @@ packages: '@tiptap/pm': 2.2.4 dev: false + /@tokenizer/token@0.3.0: + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + dev: false + /@tootallnate/once@2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} dev: false + /@tootallnate/quickjs-emscripten@0.23.0: + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + dev: false + /@transloadit/prettier-bytes@0.3.4: resolution: {integrity: sha512-8/SnIF9Q2k52mbjRVAYLranwkaDTLb+O9r4Z/uo8uNw//SjygKvvbF4BHSOuReufaAyum1q13602VcNud25Dfg==} dev: false @@ -7431,6 +8696,10 @@ packages: resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} dev: false + /@types/aws-lambda@8.10.142: + resolution: {integrity: sha512-wy2y/2hQKrS6myOS++koXg3N1Hg+LLyPjaggCFajczSHZPqBnOMuT2sdH3kiASrmdBYyM3pmjyz5SoWraRllCQ==} + dev: false + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: @@ -7473,6 +8742,10 @@ packages: '@types/node': 20.11.19 dev: false + /@types/btoa-lite@1.0.2: + resolution: {integrity: sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==} + dev: false + /@types/busboy@1.5.3: resolution: {integrity: sha512-YMBLFN/xBD8bnqywIlGyYqsNFXu6bsiY7h3Ae0kO17qEuTjsqeyYMRPSUDacIKIquws2Y6KjmxAyNx8xB3xQbw==} dependencies: @@ -7540,6 +8813,12 @@ packages: resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} dev: false + /@types/debug@4.1.12: + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + dependencies: + '@types/ms': 0.7.34 + dev: false + /@types/detect-port@1.3.5: resolution: {integrity: sha512-Rf3/lB9WkDfIL9eEKaSYKc+1L/rNVYBjThk22JTqQw0YozXarX8YljFAz+HCoC6h4B4KwCMsBPZHaFezwT4BNA==} dev: false @@ -7552,10 +8831,27 @@ packages: resolution: {integrity: sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==} dev: false + /@types/domhandler@2.4.5: + resolution: {integrity: sha512-lANhC2grmFG1gBac/8sDAKdIXx+TzAdkJIAjEOSMA+qW3297ybACEbacJnG15aNYfrzDO6fdcoouokqAKsy6aQ==} + dev: false + + /@types/domutils@1.7.8: + resolution: {integrity: sha512-iZGboDV79ibrO3D625p9yD+VgmMDnyJocdIRJvu9Xz66R8SHfOY/XNgdjY5SFoFiLgILceVfSLt7IUhlk1Vhhg==} + dependencies: + '@types/domhandler': 2.4.5 + dev: false + /@types/ejs@3.1.5: resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==} dev: false + /@types/email-addresses@3.0.0: + resolution: {integrity: sha512-jGUOSgpOEWhTH4tMCj56NZenkzER259nJ5NGRvxXld3X7Lai/lxC3QNfDM0rVGMkj+WhANMpvIf195tgwnE7wQ==} + deprecated: This is a stub types definition for email-addresses (https://github.com/jackbowman/email-addresses). email-addresses provides its own type definitions, so you don't need @types/email-addresses installed! + dependencies: + email-addresses: 5.0.0 + dev: false + /@types/emscripten@1.39.10: resolution: {integrity: sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==} dev: false @@ -7585,6 +8881,13 @@ packages: '@types/express': 4.17.21 dev: false + /@types/express-form-data@2.0.5: + resolution: {integrity: sha512-zn1Cvy/scDOgV4j5pUKxsPQQVIX5vAGL6xjLWteSApd3HAmmfbdeZ3WMbFAks0kIaAxbSIT79W9GB4AVkS6fOA==} + dependencies: + '@types/connect': 3.4.38 + '@types/multiparty': 0.0.36 + dev: false + /@types/express-serve-static-core@4.17.43: resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} dependencies: @@ -7607,6 +8910,12 @@ packages: resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==} dev: false + /@types/fs-extra@9.0.13: + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + dependencies: + '@types/node': 20.11.19 + dev: false + /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: @@ -7621,6 +8930,15 @@ packages: resolution: {integrity: sha512-QFcqfc7TiVbvIX8Fc2kWUxakruI1Ay6uitaGCYHzI5M0WHQROV5D2XeSaVrK0FmvssivXum4yERVnJsiuH61Ww==} dev: false + /@types/htmlparser2@3.10.7: + resolution: {integrity: sha512-ycBs4PNr9rY9XFFp4WkP+M1UcO49ahn0+9b24cmIY6KWy0w35rW0G8+JTTe9Rp6Wnyqn5SEHZrhCBMa0TIOxBw==} + dependencies: + '@types/domhandler': 2.4.5 + '@types/domutils': 1.7.8 + '@types/node': 20.11.19 + domhandler: 2.4.2 + dev: false + /@types/http-assert@1.5.5: resolution: {integrity: sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==} dev: false @@ -7666,6 +8984,15 @@ packages: pretty-format: 29.7.0 dev: false + /@types/jimp@0.2.28: + resolution: {integrity: sha512-nLIVbImtcaEf90y2XQsMzfgWK5EZxfDg6EVWobrkFTFJiLqmx/yU5Jh+LYUN94ztzXX1GwQLFYHaEi8tfMeZzw==} + deprecated: This is a stub types definition for jimp (https://github.com/oliver-moran/jimp#readme). jimp provides its own type definitions, so you don't need @types/jimp installed! + dependencies: + jimp: 0.16.13 + transitivePeerDependencies: + - debug + dev: false + /@types/js-yaml@4.0.9: resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} dev: false @@ -7686,6 +9013,12 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: false + /@types/jsonwebtoken@9.0.6: + resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} + dependencies: + '@types/node': 20.11.19 + dev: false + /@types/keygrip@1.0.6: resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} dev: false @@ -7814,6 +9147,16 @@ packages: '@types/node': 20.11.19 dev: false + /@types/ms@0.7.34: + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + dev: false + + /@types/multiparty@0.0.36: + resolution: {integrity: sha512-DNqs3xYFMLgQfYrYuuleox1wzpUJxOgR8+WQ99DPu1B6zLT+sCMMsWfqEFTThPrI3109q81sChUKg/QM/ZUPiw==} + dependencies: + '@types/node': 20.11.19 + dev: false + /@types/node-fetch@2.6.11: resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} dependencies: @@ -7827,6 +9170,10 @@ packages: '@types/node': 20.11.19 dev: false + /@types/node@16.9.1: + resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} + dev: false + /@types/node@18.19.17: resolution: {integrity: sha512-SzyGKgwPzuWp2SHhlpXKzCX0pIOfcI4V2eF37nNBJOhwlegQ83omtVQ1XxZpDE06V/d6AQvfQdPfnw0tRC//Ng==} dependencies: @@ -7891,6 +9238,24 @@ packages: '@types/express': 4.17.21 dev: false + /@types/pdfjs-dist@2.10.378: + resolution: {integrity: sha512-TRdIPqdsvKmPla44kVy4jv5Nt5vjMfVjbIEke1CRULIrwKNRC4lIiZvNYDJvbUMNCFPNIUcOKhXTyMJrX18IMA==} + deprecated: This is a stub types definition. pdfjs-dist provides its own type definitions, so you do not need this installed. + dependencies: + pdfjs-dist: 2.12.313 + transitivePeerDependencies: + - worker-loader + dev: false + + /@types/plist@3.0.5: + resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + requiresBuild: true + dependencies: + '@types/node': 20.11.19 + xmlbuilder: 11.0.1 + dev: false + optional: true + /@types/png-chunks-extract@1.0.2: resolution: {integrity: sha512-z6djfFIbrrddtunoMJBOPlyZrnmeuG1kkvHUNi2QfpOb+JMMLuLliHHTmMyRi7k7LiTAut0HbdGCF6ibDtQAHQ==} dev: false @@ -7998,6 +9363,14 @@ packages: resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} dev: false + /@types/strip-bom@3.0.0: + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} + dev: false + + /@types/strip-json-comments@0.0.30: + resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} + dev: false + /@types/tar-stream@2.2.3: resolution: {integrity: sha512-if3mugZfjVkXOMZdFjIHySxY13r6GXPpyOlsDmLffvyI7tLz9wXE8BFjNivXsvUeyJ1KNlOpfLnag+ISmxgxPw==} dependencies: @@ -8016,6 +9389,10 @@ packages: resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} dev: false + /@types/triple-beam@1.3.5: + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + dev: false + /@types/unist@2.0.10: resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} dev: false @@ -8028,6 +9405,12 @@ packages: resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} dev: false + /@types/verror@1.10.10: + resolution: {integrity: sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==} + requiresBuild: true + dev: false + optional: true + /@types/web-push@3.6.3: resolution: {integrity: sha512-v3oT4mMJsHeJ/rraliZ+7TbZtr5bQQuxcgD7C3/1q/zkAj29c8RE0F9lVZVu3hiQe5Z9fYcBreV7TLnfKR+4mg==} dependencies: @@ -8070,6 +9453,42 @@ packages: '@types/yargs-parser': 21.0.3 dev: false + /@types/yauzl@2.10.3: + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + requiresBuild: true + dependencies: + '@types/node': 20.11.19 + dev: false + optional: true + + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare-lite: 1.4.0 + semver: 7.6.0 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8099,6 +9518,26 @@ packages: - supports-color dev: false + /@typescript-eslint/parser@5.62.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.56.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8120,6 +9559,14 @@ packages: - supports-color dev: false + /@typescript-eslint/scope-manager@5.62.0: + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + dev: false + /@typescript-eslint/scope-manager@6.21.0: resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8128,6 +9575,26 @@ packages: '@typescript-eslint/visitor-keys': 6.21.0 dev: false + /@typescript-eslint/type-utils@5.62.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.56.0 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/type-utils@6.21.0(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8148,11 +9615,37 @@ packages: - supports-color dev: false + /@typescript-eslint/types@5.62.0: + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: false + /@typescript-eslint/types@6.21.0: resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} engines: {node: ^16.0.0 || >=18.0.0} dev: false + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.3.3): + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.6.0 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.3.3): resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8175,6 +9668,26 @@ packages: - supports-color dev: false + /@typescript-eslint/utils@5.62.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.7 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + eslint: 8.56.0 + eslint-scope: 5.1.1 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + /@typescript-eslint/utils@6.21.0(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8194,6 +9707,14 @@ packages: - typescript dev: false + /@typescript-eslint/visitor-keys@5.62.0: + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + dev: false + /@typescript-eslint/visitor-keys@6.21.0: resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8370,6 +9891,12 @@ packages: '@uppy/utils': 5.9.0 dev: false + /@vercel/webpack-asset-relocator-loader@1.7.4: + resolution: {integrity: sha512-RFFite6v51Qhj/eERru3qwUNCLybnceSChI5yiu9bhLpTemWbKPORAOExOgpO2W7IE/0UEh3aX6wTSHgDE/fdQ==} + dependencies: + resolve: 1.22.8 + dev: false + /@vitest/utils@0.34.7: resolution: {integrity: sha512-ziAavQLpCYS9sLOorGrFFKmy2gnfiNU0ZJ15TsMz/K92NAPS/rp9K4z6AJQQk5Y8adCy4Iwpxy7pQumQ/psnRg==} dependencies: @@ -8522,6 +10049,17 @@ packages: webpack-dev-server: 4.15.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)(webpack-cli@5.1.4)(webpack@5.90.3) dev: false + /@wry/equality@0.1.11: + resolution: {integrity: sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==} + dependencies: + tslib: 1.14.1 + dev: false + + /@xmldom/xmldom@0.8.10: + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + dev: false + /@xtuc/ieee754@1.2.0: resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} dev: false @@ -8562,11 +10100,25 @@ packages: dev: false optional: true + /File@0.10.2: + resolution: {integrity: sha512-gomQVTq/10wIR399uhGTWtYcYneTXbfe3p2RO/NR0MPrLkIyOaE9DCEPXihAm+72epLtXaplitwfJ/wkmj88dg==} + dependencies: + mime: 3.0.0 + dev: false + + /FileList@0.10.2: + resolution: {integrity: sha512-HCe9WvojxLiMEfa3l6jFkQJLzhzDXgQmfnKFoRvhEnsyVoIc5piAQNLyhOwsZsmf8IwDBfr5H71nB8Wi5w0XwA==} + dev: false + /abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} deprecated: Use your platform's native atob() and btoa() methods instead dev: false + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: false + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -8666,6 +10218,15 @@ packages: ajv: 8.12.0 dev: false + /ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.17.1 + dev: false + /ajv-keywords@3.5.2(ajv@6.12.6): resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -8701,6 +10262,15 @@ packages: uri-js: 4.4.1 dev: false + /ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.1 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + dev: false + /allure-js-commons@2.12.2: resolution: {integrity: sha512-bapkOHuwOYFR62aeNNwFmf8+LZSchzQ4Q8cFXWvEqP5fBTgADA+GujsRl936gjmTmKWVmKYwQfUt+PZw6tgFzw==} dependencies: @@ -8732,6 +10302,16 @@ packages: hasBin: true dev: false + /ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + dev: false + + /ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + dev: false + /ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} @@ -8771,6 +10351,10 @@ packages: engines: {node: '>=12'} dev: false + /any-base@1.1.0: + resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} + dev: false + /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -8784,6 +10368,73 @@ packages: engines: {node: '>=8'} dev: false + /apollo-link@1.2.14(graphql@14.7.0): + resolution: {integrity: sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==} + peerDependencies: + graphql: ^0.11.3 || ^0.12.3 || ^0.13.0 || ^14.0.0 || ^15.0.0 + dependencies: + apollo-utilities: 1.3.4(graphql@14.7.0) + graphql: 14.7.0 + ts-invariant: 0.4.4 + tslib: 1.14.1 + zen-observable-ts: 0.8.21 + dev: false + + /apollo-utilities@1.3.4(graphql@14.7.0): + resolution: {integrity: sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==} + peerDependencies: + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 + dependencies: + '@wry/equality': 0.1.11 + fast-json-stable-stringify: 2.1.0 + graphql: 14.7.0 + ts-invariant: 0.4.4 + tslib: 1.14.1 + dev: false + + /app-builder-bin@4.0.0: + resolution: {integrity: sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==} + dev: false + + /app-builder-lib@24.13.3(dmg-builder@24.13.3): + resolution: {integrity: sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 24.13.3 + electron-builder-squirrel-windows: 24.13.3 + dependencies: + '@develar/schema-utils': 2.6.5 + '@electron/notarize': 2.2.1 + '@electron/osx-sign': 1.0.5 + '@electron/universal': 1.5.1 + '@malept/flatpak-bundler': 0.4.0 + '@types/fs-extra': 9.0.13 + async-exit-hook: 2.0.1 + bluebird-lst: 1.0.9 + builder-util: 24.13.1 + builder-util-runtime: 9.2.4 + chromium-pickle-js: 0.2.0 + debug: 4.3.4 + dmg-builder: 24.13.3 + ejs: 3.1.9 + electron-publish: 24.13.1 + form-data: 4.0.0 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + is-ci: 3.0.1 + isbinaryfile: 5.0.2 + js-yaml: 4.1.0 + lazy-val: 1.0.5 + minimatch: 5.1.6 + read-config-file: 6.3.2 + sanitize-filename: 1.6.3 + semver: 7.6.0 + tar: 6.2.0 + temp-file: 3.4.0 + transitivePeerDependencies: + - supports-color + dev: false + /app-root-dir@1.0.2: resolution: {integrity: sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==} dev: false @@ -8844,6 +10495,18 @@ packages: is-string: 1.0.7 dev: false + /array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + is-string: 1.0.7 + dev: false + /array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} dev: false @@ -8864,6 +10527,18 @@ packages: is-string: 1.0.7 dev: false + /array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + dev: false + /array.prototype.findlastindex@1.2.4: resolution: {integrity: sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ==} engines: {node: '>= 0.4'} @@ -8895,6 +10570,17 @@ packages: es-shim-unscopables: 1.0.2 dev: false + /array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-shim-unscopables: 1.0.2 + dev: false + /arraybuffer.prototype.slice@1.0.3: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} @@ -8909,6 +10595,20 @@ packages: is-shared-array-buffer: 1.0.3 dev: false + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: false + + /arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + dev: false + + /asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + dev: false + /asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} dependencies: @@ -8918,6 +10618,13 @@ packages: safer-buffer: 2.1.2 dev: false + /assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + requiresBuild: true + dev: false + optional: true + /assert@2.1.0: resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} dependencies: @@ -8928,6 +10635,13 @@ packages: util: 0.12.5 dev: false + /ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + dependencies: + tslib: 2.6.2 + dev: false + /ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} @@ -8935,6 +10649,13 @@ packages: tslib: 2.6.2 dev: false + /astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + requiresBuild: true + dev: false + optional: true + /async-cache@1.1.0: resolution: {integrity: sha512-YDQc4vBn5NFhY6g6HhVshyi3Fy9+SQ5ePnE7JLDJn1DoL+i7ER+vMwtTNOYk9leZkYMnOwpBCWqyLDPw8Aig8g==} deprecated: No longer maintained. Use [lru-cache](http://npm.im/lru-cache) version 7.6 or higher, and provide an asynchronous `fetchMethod` option. @@ -8942,6 +10663,11 @@ packages: lru-cache: 4.1.5 dev: false + /async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + dev: false + /async-limiter@1.0.1: resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} requiresBuild: true @@ -8951,6 +10677,12 @@ packages: resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} dev: false + /async-mutex@0.3.2: + resolution: {integrity: sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==} + dependencies: + tslib: 2.6.2 + dev: false + /async-value-promise@1.1.1: resolution: {integrity: sha512-c2RFDKjJle1rHa0YxN9Ysu97/QBu3Wa+NOejJxsX+1qVDJrkD3JL/GN1B3gaILAEXJXbu/4Z1lcoCHFESe/APA==} dependencies: @@ -8969,11 +10701,23 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false + /at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: false + /atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} dev: false + /atomically@2.0.3: + resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==} + dependencies: + stubborn-fs: 1.2.5 + when-exit: 2.1.3 + dev: false + /autolinker@4.0.0: resolution: {integrity: sha512-fl5Kh6BmEEZx+IWBfEirnRUU5+cOiV0OK7PEt0RBKvJMJ8GaRseIOeDU3FKf4j3CE5HVefcjHmhYPOcaVt0bZw==} dependencies: @@ -9003,6 +10747,23 @@ packages: possible-typed-array-names: 1.0.0 dev: false + /aws-sdk@2.1664.0: + resolution: {integrity: sha512-S2IA1cCGz38d8ZKsuQGwlK3LE+9cXFt7OFsSGQtKX1Mc40xFXpiqQy7jX1r0vZIiy9ZMnxeTcBPM28G/yYu2kA==} + engines: {node: '>= 10.0.0'} + requiresBuild: true + dependencies: + buffer: 4.9.2 + events: 1.1.1 + ieee754: 1.1.13 + jmespath: 0.16.0 + querystring: 0.2.0 + sax: 1.2.1 + url: 0.10.3 + util: 0.12.5 + uuid: 8.0.0 + xml2js: 0.6.2 + dev: false + /axobject-query@4.0.0: resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} dependencies: @@ -9193,10 +10954,19 @@ packages: safe-buffer: 5.1.2 dev: false + /basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + dev: false + /batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} dev: false + /before-after-hook@2.2.3: + resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + dev: false + /better-opn@3.0.2: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} @@ -9213,6 +10983,10 @@ packages: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} dev: false + /bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + dev: false + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -9236,6 +11010,24 @@ packages: readable-stream: 3.6.2 dev: false + /bluebird-lst@1.0.9: + resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==} + dependencies: + bluebird: 3.7.2 + dev: false + + /bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + dev: false + + /bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + dev: false + + /bmp-js@0.1.0: + resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} + dev: false + /bn.js@4.12.0: resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} dev: false @@ -9269,6 +11061,16 @@ packages: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} dev: false + /boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + requiresBuild: true + dev: false + optional: true + + /bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + dev: false + /bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} dev: false @@ -9365,6 +11167,10 @@ packages: engines: {node: '>=16.20.1'} dev: false + /btoa-lite@1.0.0: + resolution: {integrity: sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==} + dev: false + /buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} dev: false @@ -9378,10 +11184,28 @@ packages: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} dev: false + /buffer-equal@0.0.1: + resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==} + engines: {node: '>=0.4.0'} + dev: false + + /buffer-equal@1.0.1: + resolution: {integrity: sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==} + engines: {node: '>=0.4'} + dev: false + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: false + /buffer@4.9.2: + resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + isarray: 1.0.0 + dev: false + /buffer@5.6.0: resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} dependencies: @@ -9396,6 +11220,23 @@ packages: ieee754: 1.2.1 dev: false + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /bufferjs@2.0.0: + resolution: {integrity: sha512-VnTCQKC+AJ61OFGe/hn3jRXoIt/B95NUcuxzAwiVT0PFB0KRZImkoDPYdFqDIs7xAs1eJ3yiKcHnuUiiYe7ucQ==} + engines: {node: '>=0.2.0'} + dev: false + + /bufferjs@3.0.1: + resolution: {integrity: sha512-qrCIGPcd9ODawCNyqR2o55zgaC/r7XHZ7oUh2s99uk+NVBS3SjIHigxS1S2KXpt8wsoQxAN55iPi8GIH8TGMRg==} + engines: {node: '>=0.2.0'} + dev: false + /bufferutil@4.0.8: resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} engines: {node: '>=6.14.2'} @@ -9404,6 +11245,39 @@ packages: node-gyp-build: 4.8.0 dev: false + /builder-util-runtime@9.2.4: + resolution: {integrity: sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==} + engines: {node: '>=12.0.0'} + dependencies: + debug: 4.3.4 + sax: 1.3.0 + transitivePeerDependencies: + - supports-color + dev: false + + /builder-util@24.13.1: + resolution: {integrity: sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==} + dependencies: + 7zip-bin: 5.2.0 + '@types/debug': 4.1.12 + app-builder-bin: 4.0.0 + bluebird-lst: 1.0.9 + builder-util-runtime: 9.2.4 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + fs-extra: 10.1.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-ci: 3.0.1 + js-yaml: 4.1.0 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + transitivePeerDependencies: + - supports-color + dev: false + /builtins@5.0.1: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} dependencies: @@ -9476,6 +11350,16 @@ packages: tslib: 2.6.2 dev: false + /camelcase-keys@9.1.3: + resolution: {integrity: sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==} + engines: {node: '>=16'} + dependencies: + camelcase: 8.0.0 + map-obj: 5.0.0 + quick-lru: 6.1.2 + type-fest: 4.23.0 + dev: false + /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} @@ -9486,6 +11370,11 @@ packages: engines: {node: '>=10'} dev: false + /camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + dev: false + /caniuse-lite@1.0.30001589: resolution: {integrity: sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==} dev: false @@ -9500,6 +11389,14 @@ packages: engines: {node: '>=4'} dev: false + /centra@2.7.0: + resolution: {integrity: sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==} + dependencies: + follow-redirects: 1.15.6 + transitivePeerDependencies: + - debug + dev: false + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -9551,6 +11448,21 @@ packages: engines: {node: '>=6.0'} dev: false + /chromium-bidi@0.6.2(devtools-protocol@0.0.1312386): + resolution: {integrity: sha512-4WVBa6ijmUTVr9cZD4eicQD8Mdy/HCX3bzEIYYpmk0glqYLoWH+LqQEvV9RpDRzoQSbY1KJHloYXbDMXMbDPhg==} + peerDependencies: + devtools-protocol: '*' + dependencies: + devtools-protocol: 0.0.1312386 + mitt: 3.0.1 + urlpattern-polyfill: 10.0.0 + zod: 3.23.8 + dev: false + + /chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + dev: false + /ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -9603,6 +11515,32 @@ packages: '@colors/colors': 1.5.0 dev: false + /cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + dev: false + optional: true + + /cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + dev: false + + /cliui@4.1.0: + resolution: {integrity: sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==} + dependencies: + string-width: 2.1.1 + strip-ansi: 4.0.0 + wrap-ansi: 2.1.0 + dev: false + /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -9646,6 +11584,11 @@ packages: engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} dev: false + /code-point-at@1.1.0: + resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} + engines: {node: '>=0.10.0'} + dev: false + /code-red@1.0.4: resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} dependencies: @@ -9688,6 +11631,13 @@ packages: simple-swizzle: 0.2.2 dev: false + /color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + dev: false + /color@4.2.3: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} @@ -9700,6 +11650,13 @@ packages: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} dev: false + /colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + dev: false + /combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -9716,6 +11673,11 @@ packages: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: false + /commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + dev: false + /commander@6.2.1: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} @@ -9731,6 +11693,11 @@ packages: engines: {node: '>= 12'} dev: false + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + dev: false + /comment-json@4.2.3: resolution: {integrity: sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==} engines: {node: '>= 6'} @@ -9750,6 +11717,15 @@ packages: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: false + /compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + dev: false + + /component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + dev: false + /compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -9795,11 +11771,44 @@ packages: typedarray: 0.0.6 dev: false + /conf@13.0.1: + resolution: {integrity: sha512-l9Uwc9eOnz39oADzGO2cSBDi7siv8lwO+31ocQ2nOJijnDiW3pxqm9VV10DPYUO28wW83DjABoUqY1nfHRR2hQ==} + engines: {node: '>=18'} + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1 + atomically: 2.0.3 + debounce-fn: 6.0.0 + dot-prop: 9.0.0 + env-paths: 3.0.0 + json-schema-typed: 8.0.1 + semver: 7.6.3 + uint8array-extras: 1.4.0 + dev: false + + /config-file-ts@0.2.6: + resolution: {integrity: sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==} + dependencies: + glob: 10.3.10 + typescript: 5.3.3 + dev: false + /connect-history-api-fallback@2.0.0: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} dev: false + /connect-multiparty@2.2.0: + resolution: {integrity: sha512-zKcpA7cuXGEhuw9Pz7JmVCFmp85jzGLGm/iiagXTwyEAJp4ypLPtRS/V4IGuGb9KjjrgHBs6P/gDCpZHnFzksA==} + engines: {node: '>=0.10.0'} + dependencies: + http-errors: 1.7.3 + multiparty: 4.2.3 + on-finished: 2.3.0 + qs: 6.5.3 + type-is: 1.6.18 + dev: false + /consola@3.2.3: resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} @@ -9851,6 +11860,10 @@ packages: engines: {node: '>= 0.6'} dev: false + /cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + dev: false + /cookies@0.9.1: resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} engines: {node: '>= 0.8'} @@ -9863,6 +11876,21 @@ packages: resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==} dev: false + /copy-webpack-plugin@11.0.0(webpack@5.90.3): + resolution: {integrity: sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==} + engines: {node: '>= 14.15.0'} + peerDependencies: + webpack: ^5.1.0 + dependencies: + fast-glob: 3.3.2 + glob-parent: 6.0.2 + globby: 13.2.2 + normalize-path: 3.0.0 + schema-utils: 4.2.0 + serialize-javascript: 6.0.2 + webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4) + dev: false + /core-js-compat@3.36.0: resolution: {integrity: sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==} dependencies: @@ -9874,6 +11902,12 @@ packages: requiresBuild: true dev: false + /core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + requiresBuild: true + dev: false + optional: true + /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: false @@ -9913,6 +11947,22 @@ packages: typescript: 5.3.3 dev: false + /cosmiconfig@9.0.0(typescript@5.3.3): + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + typescript: 5.3.3 + dev: false + /crc-32@0.3.0: resolution: {integrity: sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==} engines: {node: '>=0.8'} @@ -9971,6 +12021,17 @@ packages: - encoding dev: false + /cross-spawn@6.0.5: + resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} + engines: {node: '>=4.8'} + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -10098,10 +12159,23 @@ packages: resolution: {integrity: sha512-JL+Q6YEikT2uoe57InjFFa6VejhSv0tDwOxeQ1bVQKeUC/NCnLAAZ8n3PzowPQQLuZ37fysDYZipB2UJkH9C6A==} dev: false + /d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + dev: false + /dash-get@1.0.2: resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==} dev: false + /data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + dev: false + /data-urls@3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} @@ -10111,6 +12185,33 @@ packages: whatwg-url: 11.0.0 dev: false + /data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: false + + /data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: false + + /data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: false + /date-fns-tz@2.0.0(date-fns@2.30.0): resolution: {integrity: sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==} peerDependencies: @@ -10126,6 +12227,13 @@ packages: '@babel/runtime': 7.23.9 dev: false + /debounce-fn@6.0.0: + resolution: {integrity: sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==} + engines: {node: '>=18'} + dependencies: + mimic-function: 5.0.1 + dev: false + /debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} dev: false @@ -10154,6 +12262,23 @@ packages: ms: 2.1.2 dev: false + /debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: false + /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} dev: false @@ -10274,6 +12399,15 @@ packages: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} dev: false + /degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + dev: false + /del@6.1.1: resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} engines: {node: '>=10'} @@ -10307,6 +12441,14 @@ packages: engines: {node: '>= 0.8'} dev: false + /deprecated-decorator@0.1.6: + resolution: {integrity: sha512-MHidOOnCHGlZDKsI21+mbIIhf4Fff+hhCTB7gtVg4uoIqjcrTZc5v6M+GS2zVI0sV7PqK415rb8XaOSQsQkHOw==} + dev: false + + /deprecation@2.3.1: + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dev: false + /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -10357,21 +12499,63 @@ packages: - supports-color dev: false + /devtools-protocol@0.0.1312386: + resolution: {integrity: sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==} + dev: false + + /dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dev: false + /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: false + /diff2html@3.4.48: + resolution: {integrity: sha512-1lzNSg0G0VPKZPTyi4knzV2nAWTXBy/QaWCKzDto6iEIlcuOJEG0li4bElJfpHNz+pBqPu4AcC1i9ZCo9KMUOg==} + engines: {node: '>=12'} + dependencies: + diff: 5.1.0 + hogan.js: 3.0.2 + optionalDependencies: + highlight.js: 11.9.0 + dev: false + + /diff@3.5.0: + resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} + engines: {node: '>=0.3.1'} + dev: false + /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} dev: false + /diff@5.1.0: + resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} + engines: {node: '>=0.3.1'} + dev: false + /diff@5.2.0: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} dev: false + /dingbat-to-unicode@1.0.1: + resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} + dev: false + + /dir-compare@3.3.0: + resolution: {integrity: sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==} + dependencies: + buffer-equal: 1.0.1 + minimatch: 3.1.2 + dev: false + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -10379,6 +12563,40 @@ packages: path-type: 4.0.0 dev: false + /dmg-builder@24.13.3: + resolution: {integrity: sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==} + dependencies: + app-builder-lib: 24.13.3(dmg-builder@24.13.3) + builder-util: 24.13.1 + builder-util-runtime: 9.2.4 + fs-extra: 10.1.0 + iconv-lite: 0.6.3 + js-yaml: 4.1.0 + optionalDependencies: + dmg-license: 1.0.11 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + dev: false + + /dmg-license@1.0.11: + resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} + engines: {node: '>=8'} + os: [darwin] + hasBin: true + requiresBuild: true + dependencies: + '@types/plist': 3.0.5 + '@types/verror': 1.10.10 + ajv: 6.12.6 + crc: 3.8.0 + iconv-corefoundation: 1.1.7 + plist: 3.1.0 + smart-buffer: 4.2.0 + verror: 1.10.1 + dev: false + optional: true + /dns-packet@5.6.1: resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} @@ -10430,6 +12648,10 @@ packages: resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} dev: false + /domelementtype@1.3.1: + resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} + dev: false + /domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} dev: false @@ -10442,6 +12664,12 @@ packages: webidl-conversions: 7.0.0 dev: false + /domhandler@2.4.2: + resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==} + dependencies: + domelementtype: 1.3.1 + dev: false + /domhandler@3.3.0: resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==} engines: {node: '>= 4'} @@ -10486,6 +12714,13 @@ packages: tslib: 2.6.2 dev: false + /dot-prop@9.0.0: + resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} + engines: {node: '>=18'} + dependencies: + type-fest: 4.23.0 + dev: false + /dotenv-defaults@2.0.2: resolution: {integrity: sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg==} dependencies: @@ -10497,6 +12732,10 @@ packages: engines: {node: '>=12'} dev: false + /dotenv-expand@5.1.0: + resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} + dev: false + /dotenv-webpack@8.0.1(webpack@5.90.3): resolution: {integrity: sha512-CdrgfhZOnx4uB18SgaoP9XHRN2v48BbjuXQsZY5ixs5A8579NxQkmMxRtI7aTwSiSQcM2ao12Fdu+L3ZS3bG4w==} engines: {node: '>=10'} @@ -10517,6 +12756,17 @@ packages: engines: {node: '>=10'} dev: false + /dotenv@9.0.2: + resolution: {integrity: sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==} + engines: {node: '>=10'} + dev: false + + /duck@0.1.12: + resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==} + dependencies: + underscore: 1.13.7 + dev: false + /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} dev: false @@ -10530,6 +12780,12 @@ packages: stream-shift: 1.0.3 dev: false + /dynamic-dedupe@0.3.0: + resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + dependencies: + xtend: 4.0.2 + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: false @@ -10613,10 +12869,119 @@ packages: - supports-color dev: false + /electron-builder@24.13.3: + resolution: {integrity: sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + app-builder-lib: 24.13.3(dmg-builder@24.13.3) + builder-util: 24.13.1 + builder-util-runtime: 9.2.4 + chalk: 4.1.2 + dmg-builder: 24.13.3 + fs-extra: 10.1.0 + is-ci: 3.0.1 + lazy-val: 1.0.5 + read-config-file: 6.3.2 + simple-update-notifier: 2.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + dev: false + + /electron-context-menu@4.0.1: + resolution: {integrity: sha512-tPRzsIK48gm+DDP4FLJeocrFg1rJBH7ZgQx3j4ebt5d6euM2ERAB8pdBmKyhHy7na/BiZfrw4ASMq97oVwQXgw==} + engines: {node: '>=18'} + dependencies: + cli-truncate: 4.0.0 + electron-dl: 4.0.0 + electron-is-dev: 3.0.1 + dev: false + + /electron-dl@4.0.0: + resolution: {integrity: sha512-USiB9816d2JzKv0LiSbreRfTg5lDk3lWh0vlx/gugCO92ZIJkHVH0UM18EHvKeadErP6Xn4yiTphWzYfbA2Ong==} + engines: {node: '>=18'} + dependencies: + ext-name: 5.0.0 + pupa: 3.1.0 + unused-filename: 4.0.1 + dev: false + + /electron-is-dev@3.0.1: + resolution: {integrity: sha512-8TjjAh8Ec51hUi3o4TaU0mD3GMTOESi866oRNavj9A3IQJ7pmv+MJVmdZBFGw4GFT36X7bkqnuDNYvkQgvyI8Q==} + engines: {node: '>=18'} + dev: false + + /electron-log@5.1.7: + resolution: {integrity: sha512-/PjrS9zGkrZCDTHt6IgNE3FeciBbi4wd7U76NG9jAoNXF99E9IJdvBkqvaUJ1NjLojYDKs0kTvn9YhKy1/Zi+Q==} + engines: {node: '>= 14'} + dev: false + + /electron-publish@24.13.1: + resolution: {integrity: sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==} + dependencies: + '@types/fs-extra': 9.0.13 + builder-util: 24.13.1 + builder-util-runtime: 9.2.4 + chalk: 4.1.2 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + dev: false + + /electron-squirrel-startup@1.0.1: + resolution: {integrity: sha512-sTfFIHGku+7PsHLJ7v0dRcZNkALrV+YEozINTW8X1nM//e5O3L+rfYuvSW00lmGHnYmUjARZulD8F2V8ISI9RA==} + dependencies: + debug: 2.6.9 + dev: false + + /electron-store@10.0.0: + resolution: {integrity: sha512-BU/QZh+5twHBprRdLu3YZX/rIarmZzhTNpJvAvqG1/yN0mNCrsMh0kl7bM4xaUKDNRiHz1r7wP/7Prjh7cleIw==} + engines: {node: '>=20'} + dependencies: + conf: 13.0.1 + type-fest: 4.23.0 + dev: false + /electron-to-chromium@1.4.679: resolution: {integrity: sha512-NhQMsz5k0d6m9z3qAxnsOR/ebal4NAGsrNVRwcDo4Kc/zQ7KdsTKZUxZoygHcVRb0QDW3waEDIcE3isZ79RP6g==} dev: false + /electron-updater@6.2.1: + resolution: {integrity: sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==} + dependencies: + builder-util-runtime: 9.2.4 + fs-extra: 10.1.0 + js-yaml: 4.1.0 + lazy-val: 1.0.5 + lodash.escaperegexp: 4.1.2 + lodash.isequal: 4.5.0 + semver: 7.6.0 + tiny-typed-emitter: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /electron-windows-badge@1.1.0: + resolution: {integrity: sha512-hWFbUk9u3fQHcKzTAcjZAN7XH9bL9oH9g20RRDU/DVDNqdMI03GzlBZfR/R8R1krYu9AT4biLqSCAxnt9LMAfA==} + dev: false + + /electron@31.3.0: + resolution: {integrity: sha512-3LMRMmK4UK0A+jYSLGLYdfhc20TgY2v5jD3iGmhRZlDYj0gn7xBj/waRjlNalysZ0D2rgPvoes0wHuf5e/Bguw==} + engines: {node: '>= 12.20.55'} + hasBin: true + requiresBuild: true + dependencies: + '@electron/get': 2.0.3 + '@types/node': 20.11.19 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + /email-addresses@5.0.0: resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==} dev: false @@ -10643,6 +13008,10 @@ packages: engines: {node: '>= 4'} dev: false + /enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + dev: false + /encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -10679,12 +13048,26 @@ packages: engines: {node: '>=0.12'} dev: false + /env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + dev: false + + /env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + /envinfo@7.11.1: resolution: {integrity: sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==} engines: {node: '>=4'} hasBin: true dev: false + /err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + dev: false + /error-callsites@2.0.4: resolution: {integrity: sha512-V877Ch4FC4FN178fDK1fsrHN4I1YQIBdtjKrHh3BUHMnh3SMvwUVrqkaOgDpUuevgSNna0RBq6Ox9SGlxYrigA==} engines: {node: '>=6.x'} @@ -10749,6 +13132,58 @@ packages: which-typed-array: 1.1.14 dev: false + /es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + dev: false + /es-array-method-boxes-properly@1.0.0: resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} dev: false @@ -10779,10 +13214,37 @@ packages: stop-iteration-iterator: 1.0.0 dev: false + /es-iterator-helpers@1.0.19: + resolution: {integrity: sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-set-tostringtag: 2.0.3 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + globalthis: 1.0.3 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + iterator.prototype: 1.1.2 + safe-array-concat: 1.1.2 + dev: false + /es-module-lexer@1.4.1: resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} dev: false + /es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: false + /es-set-tostringtag@2.0.3: resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} engines: {node: '>= 0.4'} @@ -10807,10 +13269,43 @@ packages: is-symbol: 1.0.4 dev: false + /es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + requiresBuild: true + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + dev: false + + /es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + requiresBuild: true + dev: false + optional: true + + /es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + dev: false + /es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} dev: false + /es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + dependencies: + d: 1.0.2 + ext: 1.7.0 + dev: false + /esbuild-loader@4.0.3(webpack@5.90.3): resolution: {integrity: sha512-YpaSRisj7TSg6maKKKG9OJGGm0BZ7EXeov8J8cXEYdugjlAJ0wL7aj2JactoQvPJ113v2Ar204pdJWrZsAQc8Q==} peerDependencies: @@ -10959,6 +13454,11 @@ packages: engines: {node: '>=6'} dev: false + /escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + dev: false + /escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} dev: false @@ -10978,6 +13478,11 @@ packages: engines: {node: '>=10'} dev: false + /escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: false + /escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} @@ -11008,6 +13513,39 @@ packages: eslint: 8.56.0 dev: false + /eslint-config-standard-jsx@11.0.0(eslint-plugin-react@7.35.0)(eslint@8.56.0): + resolution: {integrity: sha512-+1EV/R0JxEK1L0NGolAr8Iktm3Rgotx3BKwgaX+eAuSX8D952LULKtjgZD3F+e6SvibONnhLwoTi9DPxN5LvvQ==} + peerDependencies: + eslint: ^8.8.0 + eslint-plugin-react: ^7.28.0 + dependencies: + eslint: 8.56.0 + eslint-plugin-react: 7.35.0(eslint@8.56.0) + dev: false + + /eslint-config-standard-with-typescript@23.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-iaaWifImn37Z1OXbNW1es7KI+S7D408F9ys0bpaQf2temeBWlvb0Nc5qHkOgYaRb5QxTZT32GGeN1gtswASOXA==} + deprecated: Please use eslint-config-love, instead. + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.0.0 + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: ^15.0.0 + eslint-plugin-promise: ^6.0.0 + typescript: '*' + dependencies: + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard: 17.0.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false + /eslint-config-standard-with-typescript@40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-GXUJcwIXiTQaS3H4etv8a1lejVVdZYaxZNz3g7vt6GoJosQqMTurbmSC4FVGyHiGT/d1TjFr3+47A3xsHhsG+Q==} peerDependencies: @@ -11030,6 +13568,20 @@ packages: - supports-color dev: false + /eslint-config-standard@17.0.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0): + resolution: {integrity: sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==} + peerDependencies: + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: ^15.0.0 + eslint-plugin-promise: ^6.0.0 + dependencies: + eslint: 8.56.0 + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + dev: false + /eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0): resolution: {integrity: sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==} engines: {node: '>=12.0.0'} @@ -11066,6 +13618,17 @@ packages: eslint: 8.56.0 dev: false + /eslint-plugin-es@3.0.1(eslint@8.56.0): + resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + eslint: 8.56.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + dev: false + /eslint-plugin-es@4.1.0(eslint@8.56.0): resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} engines: {node: '>=8.10.0'} @@ -11120,6 +13683,21 @@ packages: semver: 7.6.0 dev: false + /eslint-plugin-node@11.1.0(eslint@8.56.0): + resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=5.16.0' + dependencies: + eslint: 8.56.0 + eslint-plugin-es: 3.0.1(eslint@8.56.0) + eslint-utils: 2.1.0 + ignore: 5.3.1 + minimatch: 3.1.2 + resolve: 1.22.8 + semver: 6.3.1 + dev: false + /eslint-plugin-promise@6.1.1(eslint@8.56.0): resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -11129,6 +13707,33 @@ packages: eslint: 8.56.0 dev: false + /eslint-plugin-react@7.35.0(eslint@8.56.0): + resolution: {integrity: sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + dependencies: + array-includes: 3.1.8 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.0.19 + eslint: 8.56.0 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.8 + object.fromentries: 2.0.8 + object.values: 1.2.0 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.11 + string.prototype.repeat: 1.0.0 + dev: false + /eslint-plugin-svelte@2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2): resolution: {integrity: sha512-IF8TpLnROSGy98Z3NrsKXWDSCbNY2ReHDcrYTuXZMbfX7VmESISR78TWgO9zdg4Dht1X8coub5jKwHzP0ExRug==} engines: {node: ^14.17.0 || >=16.0.0} @@ -11310,6 +13915,16 @@ packages: - supports-color dev: false + /esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + dev: false + /espree@9.2.0: resolution: {integrity: sha512-oP3utRkynpZWF/F2x/HZJ+AGtnIclaR7z1pYPxy7NYM2fSO6LgK/Rkny8anRSPK/VwEA1eqm2squui0T7ZMOBg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -11374,6 +13989,13 @@ packages: engines: {node: '>= 0.6'} dev: false + /event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + dev: false + /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: false @@ -11382,11 +14004,34 @@ packages: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} dev: false + /events@1.1.1: + resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} + engines: {node: '>=0.4.x'} + dev: false + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} dev: false + /eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + dev: false + + /execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + dependencies: + cross-spawn: 6.0.5 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + dev: false + /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -11417,6 +14062,10 @@ packages: strip-final-newline: 3.0.0 dev: false + /exif-parser@0.1.12: + resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} + dev: false + /exifr@7.1.3: resolution: {integrity: sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==} dev: false @@ -11449,6 +14098,15 @@ packages: busboy: 1.6.0 dev: false + /express-form-data@2.0.23: + resolution: {integrity: sha512-Efm2t+xL6vEqUPs4kAyBY9xG/8LGpbSVnnFLZw6HgdxHzSRlg2/sNlA2UdMlcIKMH+LDOn7NKXUDGJK125F4rw==} + engines: {node: '>=10.7.0'} + dependencies: + connect-multiparty: 2.2.0 + fs-extra: 9.1.0 + signal-exit: 3.0.7 + dev: false + /express-static-gzip@2.1.7: resolution: {integrity: sha512-QOCZUC+lhPPCjIJKpQGu1Oa61Axg9Mq09Qvit8Of7kzpMuwDeMSqjjQteQS3OVw/GkENBoSBheuQDWPlngImvw==} dependencies: @@ -11492,6 +14150,27 @@ packages: vary: 1.1.2 dev: false + /ext-list@2.2.2: + resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} + engines: {node: '>=0.10.0'} + dependencies: + mime-db: 1.52.0 + dev: false + + /ext-name@5.0.0: + resolution: {integrity: sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==} + engines: {node: '>=4'} + dependencies: + ext-list: 2.2.2 + sort-keys-length: 1.0.1 + dev: false + + /ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + dependencies: + type: 2.7.3 + dev: false + /extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} dev: false @@ -11506,6 +14185,27 @@ packages: yauzl: 2.10.0 dev: false + /extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + dependencies: + debug: 4.3.5 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + dev: false + + /extsprintf@1.4.1: + resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} + engines: {'0': node >=0.6.0} + requiresBuild: true + dev: false + optional: true + /fast-copy@3.0.1: resolution: {integrity: sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==} dev: false @@ -11557,6 +14257,14 @@ packages: end-of-stream: 1.4.4 dev: false + /fast-text-encoding@1.0.6: + resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==} + dev: false + + /fast-uri@3.0.1: + resolution: {integrity: sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==} + dev: false + /fast-xml-parser@4.2.5: resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==} hasBin: true @@ -11601,14 +14309,35 @@ packages: pend: 1.2.0 dev: false + /fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + dev: false + /fetch-retry@5.0.6: resolution: {integrity: sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==} dev: false + /fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + dev: false + /fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} dev: false + /file-api@0.10.4: + resolution: {integrity: sha512-RVBXJGmsnQxokdpy264pmsdBjbUuxE6QT2xxhOrO2pzwTetbTNoWVFgkONFWmopm5mellsXrQIQhMY9fjufi9g==} + dependencies: + File: 0.10.2 + FileList: 0.10.2 + bufferjs: 3.0.1 + file-error: 0.10.2 + filereader: 0.10.3 + formdata: 0.10.4 + mime: 3.0.0 + remedial: 1.0.8 + dev: false + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -11616,6 +14345,10 @@ packages: flat-cache: 3.2.0 dev: false + /file-error@0.10.2: + resolution: {integrity: sha512-hJsQ7sEz6dM4vuRS7cipKiixV6EymEXHe+TCf2XVWsGTOehzrmcqqKMgeYTmV24XhjWSj+pQj1e5yjPJ7DtQvw==} + dev: false + /file-loader@6.2.0(webpack@5.90.3): resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} engines: {node: '>= 10.13.0'} @@ -11627,6 +14360,12 @@ packages: webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4) dev: false + /file-stream-rotator@0.6.1: + resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} + dependencies: + moment: 2.30.1 + dev: false + /file-system-cache@2.3.0: resolution: {integrity: sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==} dependencies: @@ -11634,12 +14373,25 @@ packages: ramda: 0.29.0 dev: false + /file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + dependencies: + readable-web-to-node-stream: 3.0.2 + strtok3: 6.3.0 + token-types: 4.2.1 + dev: false + /filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} dependencies: minimatch: 5.1.6 dev: false + /filereader@0.10.3: + resolution: {integrity: sha512-7F8w6GSXuHLN80ukaVOcHgBaiTRHUZr8GeEhNdqfAECcnBoROg4i8hTl+KqtF4yUPffOJVHEFg4iDJb7xIYFng==} + dev: false + /filesize@8.0.7: resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} engines: {node: '>= 0.4.0'} @@ -11754,6 +14506,10 @@ packages: engines: {node: '>=0.4.0'} dev: false + /fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + dev: false + /follow-redirects@1.15.5: resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} engines: {node: '>=4.0'} @@ -11764,12 +14520,26 @@ packages: optional: true dev: false + /follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 dev: false + /foreachasync@3.0.0: + resolution: {integrity: sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==} + dev: false + /foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} @@ -11846,6 +14616,26 @@ packages: mime-types: 2.1.35 dev: false + /formdata@0.10.4: + resolution: {integrity: sha512-IsHa+GYLLXHx0RmpUmzQTdwxDjNinxD+1zKOYPLaRwiqTfex5caQhOzgPIjFgJkL0O884Ers76BSHzXJxHvPLw==} + dependencies: + File: 0.10.2 + FileList: 0.10.2 + bufferjs: 2.0.0 + filereader: 0.10.3 + foreachasync: 3.0.0 + remedial: 1.0.8 + dev: false + + /formidable@2.1.2: + resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} + dependencies: + dezalgo: 1.0.4 + hexoid: 1.0.0 + once: 1.4.0 + qs: 6.11.2 + dev: false + /forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} dev: false @@ -11895,6 +14685,25 @@ packages: universalify: 2.0.1 dev: false + /fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: false + + /fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: false + /fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -11948,16 +14757,49 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: false + /gaxios@5.1.3: + resolution: {integrity: sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==} + engines: {node: '>=12'} + dependencies: + extend: 3.0.2 + https-proxy-agent: 5.0.1 + is-stream: 2.0.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /gcp-metadata@5.3.0: + resolution: {integrity: sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==} + engines: {node: '>=12'} + dependencies: + gaxios: 5.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} dev: false + /get-caller-file@1.0.3: + resolution: {integrity: sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==} + dev: false + /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} dev: false + /get-east-asian-width@1.2.0: + resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} + engines: {node: '>=18'} + dev: false + /get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: false @@ -11993,6 +14835,18 @@ packages: engines: {node: '>=8'} dev: false + /get-stdin@8.0.0: + resolution: {integrity: sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==} + engines: {node: '>=10'} + dev: false + + /get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + dependencies: + pump: 3.0.0 + dev: false + /get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -12025,6 +14879,25 @@ packages: resolve-pkg-maps: 1.0.0 dev: false + /get-uri@6.0.3: + resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} + engines: {node: '>= 14'} + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.3.5 + fs-extra: 11.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /gifwrap@0.9.4: + resolution: {integrity: sha512-MDMwbhASQuVeD4JKd1fKgNgCRL3fGqMM4WaqpNhWO0JiMOAjbQdumbs4BbBZEy9/M00EHEjKN3HieVhCUlwjeQ==} + dependencies: + image-q: 4.0.0 + omggif: 1.0.10 + dev: false + /giget@1.2.1: resolution: {integrity: sha512-4VG22mopWtIeHwogGSy1FViXVo0YT+m6BrqZfz0JJFwbSsePsCdOzdLIIli5BtMp7Xe8f/o2OmBpQX2NBOC24g==} hasBin: true @@ -12088,6 +14961,20 @@ packages: path-is-absolute: 1.0.1 dev: false + /global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + requiresBuild: true + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.6.3 + serialize-error: 7.0.1 + dev: false + optional: true + /global@4.4.0: resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} dependencies: @@ -12126,6 +15013,69 @@ packages: slash: 3.0.0 dev: false + /globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 4.0.0 + dev: false + + /google-auth-library@8.9.0: + resolution: {integrity: sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==} + engines: {node: '>=12'} + dependencies: + arrify: 2.0.1 + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + fast-text-encoding: 1.0.6 + gaxios: 5.1.3 + gcp-metadata: 5.3.0 + gtoken: 6.1.2 + jws: 4.0.0 + lru-cache: 6.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /google-p12-pem@4.0.1: + resolution: {integrity: sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + node-forge: 1.3.1 + dev: false + + /googleapis-common@6.0.4: + resolution: {integrity: sha512-m4ErxGE8unR1z0VajT6AYk3s6a9gIMM6EkDZfkPnES8joeOlEtFEJeF8IyZkb0tjPXkktUfYrE4b3Li1DNyOwA==} + engines: {node: '>=12.0.0'} + dependencies: + extend: 3.0.2 + gaxios: 5.1.3 + google-auth-library: 8.9.0 + qs: 6.11.2 + url-template: 2.0.8 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /googleapis@122.0.0: + resolution: {integrity: sha512-n8Gt7j9LzSkhQEGPOrcLBKxllTvW/0v6oILuwszL/zqgelNsGJYXVqPJllgJJ6RM7maJ6T35UBeYqI6GQ/IlJg==} + engines: {node: '>=12.0.0'} + dependencies: + google-auth-library: 8.9.0 + googleapis-common: 6.0.4 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: @@ -12157,6 +15107,74 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: false + /graphql-binding@2.5.2: + resolution: {integrity: sha512-CUDidmx/Ql6bB3Jz9owZK4TlSfD58/dJEXeGVPn6Zl0qrLjxZI+jhovxYw4ArU6cLG/NdadsSg62PlR1M6+reg==} + hasBin: true + dependencies: + graphql: 14.7.0 + graphql-import: 0.7.1(graphql@14.7.0) + graphql-tools: 4.0.5(graphql@14.7.0) + iterall: 1.2.2 + object-path-immutable: 3.1.1 + resolve-cwd: 2.0.0 + ts-node: 7.0.1 + yargs: 12.0.5 + dev: false + + /graphql-import@0.7.1(graphql@14.7.0): + resolution: {integrity: sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw==} + engines: {node: '>=4.0.0'} + deprecated: GraphQL Import has been deprecated and merged into GraphQL Tools, so it will no longer get updates. Use GraphQL Tools instead to stay up-to-date! Check out https://www.graphql-tools.com/docs/migration-from-import for migration and https://the-guild.dev/blog/graphql-tools-v6 for new changes. + peerDependencies: + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 + dependencies: + graphql: 14.7.0 + lodash: 4.17.21 + resolve-from: 4.0.0 + dev: false + + /graphql-tools@4.0.5(graphql@14.7.0): + resolution: {integrity: sha512-kQCh3IZsMqquDx7zfIGWBau42xe46gmqabwYkpPlCLIjcEY1XK+auP7iGRD9/205BPyoQdY8hT96MPpgERdC9Q==} + deprecated: |- + This package has been deprecated and now it only exports makeExecutableSchema. + And it will no longer receive updates. + We recommend you to migrate to scoped packages such as @graphql-tools/schema, @graphql-tools/utils and etc. + Check out https://www.graphql-tools.com to learn what package you should use instead + peerDependencies: + graphql: ^0.13.0 || ^14.0.0 + dependencies: + apollo-link: 1.2.14(graphql@14.7.0) + apollo-utilities: 1.3.4(graphql@14.7.0) + deprecated-decorator: 0.1.6 + graphql: 14.7.0 + iterall: 1.2.2 + uuid: 3.4.0 + dev: false + + /graphql@14.7.0: + resolution: {integrity: sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA==} + engines: {node: '>= 6.x'} + dependencies: + iterall: 1.2.2 + dev: false + + /graphql@16.9.0: + resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + dev: false + + /gtoken@6.1.2: + resolution: {integrity: sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==} + engines: {node: '>=12.0.0'} + dependencies: + gaxios: 5.1.3 + google-p12-pem: 4.0.1 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /gunzip-maybe@1.4.2: resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} hasBin: true @@ -12242,15 +15260,54 @@ packages: function-bind: 1.1.2 dev: false + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true dev: false + /hexoid@1.0.0: + resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} + engines: {node: '>=8'} + dev: false + + /highlight.js@11.8.0: + resolution: {integrity: sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==} + engines: {node: '>=12.0.0'} + dev: false + + /highlight.js@11.9.0: + resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dev: false + optional: true + + /hogan.js@3.0.2: + resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==} + hasBin: true + dependencies: + mkdirp: 0.3.0 + nopt: 1.0.10 + dev: false + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: false + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: false + /hpack.js@2.1.6: resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} dependencies: @@ -12351,6 +15408,15 @@ packages: entities: 4.5.0 dev: false + /htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: false + /http-assert@1.5.0: resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} engines: {node: '>= 0.8'} @@ -12377,6 +15443,17 @@ packages: statuses: 1.5.0 dev: false + /http-errors@1.7.3: + resolution: {integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==} + engines: {node: '>= 0.6'} + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.1.1 + statuses: 1.5.0 + toidentifier: 1.0.0 + dev: false + /http-errors@1.8.1: resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} engines: {node: '>= 0.6'} @@ -12420,6 +15497,16 @@ packages: - supports-color dev: false + /http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.1 + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + dev: false + /http-proxy-middleware@2.0.6(@types/express@4.17.21): resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==} engines: {node: '>=12.0.0'} @@ -12493,6 +15580,16 @@ packages: - supports-color dev: false + /https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.1 + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + dev: false + /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -12503,6 +15600,17 @@ packages: engines: {node: '>=16.17.0'} dev: false + /iconv-corefoundation@1.1.7: + resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} + engines: {node: ^8.11.2 || >=10} + os: [darwin] + requiresBuild: true + dependencies: + cli-truncate: 2.1.0 + node-addon-api: 1.7.2 + dev: false + optional: true + /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -12526,6 +15634,10 @@ packages: postcss: 8.4.35 dev: false + /ieee754@1.1.13: + resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: false @@ -12540,6 +15652,16 @@ packages: engines: {node: '>= 4'} dev: false + /image-q@4.0.0: + resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} + dependencies: + '@types/node': 16.9.1 + dev: false + + /immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + dev: false + /immutable@4.3.5: resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==} dev: false @@ -12624,6 +15746,19 @@ packages: loose-envify: 1.4.0 dev: false + /invert-kv@2.0.0: + resolution: {integrity: sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==} + engines: {node: '>=4'} + dev: false + + /ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + dev: false + /ip@2.0.1: resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} dev: false @@ -12667,6 +15802,13 @@ packages: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} dev: false + /is-async-function@2.0.0: + resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: false + /is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} dependencies: @@ -12693,6 +15835,13 @@ packages: engines: {node: '>= 0.4'} dev: false + /is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + dependencies: + ci-info: 3.9.0 + dev: false + /is-class-hotfix@0.0.6: resolution: {integrity: sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==} dev: false @@ -12703,6 +15852,13 @@ packages: hasown: 2.0.1 dev: false + /is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + dependencies: + is-typed-array: 1.1.13 + dev: false + /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} @@ -12732,16 +15888,39 @@ packages: engines: {node: '>=0.10.0'} dev: false + /is-finalizationregistry@1.0.2: + resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} + dependencies: + call-bind: 1.0.7 + dev: false + /is-finite@1.1.0: resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} engines: {node: '>=0.10.0'} dev: false + /is-fullwidth-code-point@1.0.0: + resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} + engines: {node: '>=0.10.0'} + dependencies: + number-is-nan: 1.0.1 + dev: false + + /is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + dev: false + /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} dev: false + /is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + dev: false + /is-function@1.0.2: resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==} dev: false @@ -12840,6 +16019,11 @@ packages: engines: {node: '>=8'} dev: false + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: false + /is-plain-obj@3.0.0: resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} engines: {node: '>=10'} @@ -12852,6 +16036,13 @@ packages: isobject: 3.0.1 dev: false + /is-plain-object@3.0.0: + resolution: {integrity: sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 4.0.0 + dev: false + /is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -12890,6 +16081,11 @@ packages: call-bind: 1.0.7 dev: false + /is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + dev: false + /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -12929,6 +16125,10 @@ packages: which-typed-array: 1.1.14 dev: false + /is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + dev: false + /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -12966,6 +16166,16 @@ packages: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} dev: false + /isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + dev: false + + /isbinaryfile@5.0.2: + resolution: {integrity: sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==} + engines: {node: '>= 18.0.0'} + dev: false + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: false @@ -13047,6 +16257,20 @@ packages: istanbul-lib-report: 3.0.1 dev: false + /iterall@1.2.2: + resolution: {integrity: sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==} + dev: false + + /iterator.prototype@1.1.2: + resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} + dependencies: + define-properties: 1.2.1 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + reflect.getprototypeof: 1.0.6 + set-function-name: 2.0.2 + dev: false + /jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} @@ -13518,11 +16742,36 @@ packages: - ts-node dev: false + /jimp@0.16.13: + resolution: {integrity: sha512-Bxz8q7V4rnCky9A0ktTNGA9SkNFVWRHodddI/DaAWZJzF7sVUlFYKQ60y9JGqrKpi48ECA/TnfMzzc5C70VByA==} + dependencies: + '@babel/runtime': 7.23.9 + '@jimp/custom': 0.16.13 + '@jimp/plugins': 0.16.13(@jimp/custom@0.16.13) + '@jimp/types': 0.16.13(@jimp/custom@0.16.13) + regenerator-runtime: 0.13.11 + transitivePeerDependencies: + - debug + dev: false + /jiti@1.21.0: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true dev: false + /jmespath@0.16.0: + resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} + engines: {node: '>= 0.6.0'} + dev: false + + /jose@5.6.3: + resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==} + dev: false + + /jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + dev: false + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: false @@ -13542,6 +16791,10 @@ packages: argparse: 2.0.1 dev: false + /jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + dev: false + /jscodeshift@0.15.2(@babel/preset-env@7.23.9): resolution: {integrity: sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==} hasBin: true @@ -13628,10 +16881,20 @@ packages: hasBin: true dev: false + /json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + dependencies: + bignumber.js: 9.1.2 + dev: false + /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: false + /json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: false + /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: false @@ -13644,10 +16907,20 @@ packages: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} dev: false + /json-schema-typed@8.0.1: + resolution: {integrity: sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==} + dev: false + /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: false + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + requiresBuild: true + dev: false + optional: true + /json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -13661,6 +16934,12 @@ packages: hasBin: true dev: false + /jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.11 + dev: false + /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -13669,10 +16948,53 @@ packages: graceful-fs: 4.2.11 dev: false + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.0 + dev: false + + /jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.8 + array.prototype.flat: 1.3.2 + object.assign: 4.1.5 + object.values: 1.2.0 + dev: false + + /jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + dev: false + /just-clone@6.2.0: resolution: {integrity: sha512-1IynUYEc/HAwxhi3WDpIpxJbZpMCvvrrmZVqvj9EhpvbH8lls7HhdhiByjL7DkAaWlLIzpC0Xc/VPvy/UxLNjA==} dev: false + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + /jwa@2.0.0: resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} dependencies: @@ -13681,6 +17003,13 @@ packages: safe-buffer: 5.2.1 dev: false + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} dependencies: @@ -13815,6 +17144,10 @@ packages: - supports-color dev: false + /kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + dev: false + /launch-editor@2.6.1: resolution: {integrity: sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==} dependencies: @@ -13831,6 +17164,17 @@ packages: dotenv-expand: 10.0.0 dev: false + /lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + dev: false + + /lcid@2.0.0: + resolution: {integrity: sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==} + engines: {node: '>=6'} + dependencies: + invert-kv: 2.0.0 + dev: false + /leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} dev: false @@ -13874,6 +17218,18 @@ packages: resolution: {integrity: sha512-d0GdKshNnyfl5gM7kZ9rXjGiAbxT/zCXp0k+EAzh8H4zrb2R7GXtMCrULrX7UQxtfx6CLy/vz/lomvW79FAFdA==} dev: false + /lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + dependencies: + immediate: 3.0.6 + dev: false + + /lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + dependencies: + immediate: 3.0.6 + dev: false + /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -13911,6 +17267,46 @@ packages: webrtc-adapter: 8.2.3 dev: false + /livekit-server-sdk@2.6.0: + resolution: {integrity: sha512-lt9VZN8vTPG/P5tj4ofwLj/HTbt343AThbAWFk70OYbQucAOdv+8w00Wv9RjMndSkHaXheXOQHg8fhGOlYXmug==} + engines: {node: '>=19'} + dependencies: + '@livekit/protocol': 1.19.1 + camelcase-keys: 9.1.3 + jose: 5.6.3 + dev: false + + /load-bmfont@1.4.2: + resolution: {integrity: sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==} + dependencies: + buffer-equal: 0.0.1 + mime: 1.6.0 + parse-bmfont-ascii: 1.0.6 + parse-bmfont-binary: 1.0.6 + parse-bmfont-xml: 1.1.6 + phin: 3.7.1 + xhr: 2.6.0 + xtend: 4.0.2 + transitivePeerDependencies: + - debug + dev: false + + /load-json-file@5.3.0: + resolution: {integrity: sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==} + engines: {node: '>=6'} + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 4.0.1 + strip-bom: 3.0.0 + type-fest: 0.3.1 + dev: false + + /load-json-file@7.0.1: + resolution: {integrity: sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + /load-source-map@2.0.0: resolution: {integrity: sha512-QNZzJ2wMrTmCdeobMuMNEXHN1QGk8HG6louEkzD/zwQ7EU2RarrzlhQ4GnUYEFzLhK+Jq7IGyF/qy+XYBSO7AQ==} engines: {node: '>= 8'} @@ -13932,6 +17328,12 @@ packages: json5: 2.2.3 dev: false + /localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + dependencies: + lie: 3.1.1 + dev: false + /locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} dev: false @@ -13969,6 +17371,38 @@ packages: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} dev: false + /lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + dev: false + + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} dev: false @@ -13977,6 +17411,10 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: false + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: false @@ -13989,6 +17427,18 @@ packages: is-unicode-supported: 0.1.0 dev: false + /logform@2.6.1: + resolution: {integrity: sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.4.3 + triple-beam: 1.4.1 + dev: false + /loglevel@1.9.1: resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==} engines: {node: '>= 0.6.0'} @@ -14001,6 +17451,14 @@ packages: js-tokens: 4.0.0 dev: false + /lop@0.4.1: + resolution: {integrity: sha512-9xyho9why2A2tzm5aIcMWKvzqKsnxrf9B5I+8O30olh6lQU8PH978LqZoI4++37RBgS1Em5i54v1TFs/3wnmXQ==} + dependencies: + duck: 0.1.12 + option: 0.2.4 + underscore: 1.13.7 + dev: false + /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: @@ -14043,6 +17501,11 @@ packages: yallist: 4.0.0 dev: false + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: false + /lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -14087,6 +17550,35 @@ packages: tmpl: 1.0.5 dev: false + /mammoth@1.8.0: + resolution: {integrity: sha512-pJNfxSk9IEGVpau+tsZFz22ofjUsl2mnA5eT8PjPs2n0BP+rhVte4Nez6FdgEuxv3IGI3afiV46ImKqTGDVlbA==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + '@xmldom/xmldom': 0.8.10 + argparse: 1.0.10 + base64-js: 1.5.1 + bluebird: 3.4.7 + dingbat-to-unicode: 1.0.1 + jszip: 3.10.1 + lop: 0.4.1 + path-is-absolute: 1.0.1 + underscore: 1.13.7 + xmlbuilder: 10.1.1 + dev: false + + /map-age-cleaner@0.1.3: + resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} + engines: {node: '>=6'} + dependencies: + p-defer: 1.0.0 + dev: false + + /map-obj@5.0.0: + resolution: {integrity: sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + /map-or-similar@1.5.0: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} dev: false @@ -14116,6 +17608,15 @@ packages: react: 18.2.0 dev: false + /matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + escape-string-regexp: 4.0.0 + dev: false + optional: true + /mdast-util-definitions@4.0.0: resolution: {integrity: sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==} dependencies: @@ -14161,6 +17662,15 @@ packages: engines: {node: '>= 0.6'} dev: false + /mem@4.3.0: + resolution: {integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==} + engines: {node: '>=6'} + dependencies: + map-age-cleaner: 0.1.3 + mimic-fn: 2.1.0 + p-is-promise: 2.1.0 + dev: false + /memfs@3.5.3: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} @@ -14238,6 +17748,12 @@ packages: hasBin: true dev: false + /mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: false + /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -14248,6 +17764,11 @@ packages: engines: {node: '>=12'} dev: false + /mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + dev: false + /mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} @@ -14353,10 +17874,19 @@ packages: yallist: 4.0.0 dev: false + /mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + dev: false + /mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} dev: false + /mkdirp@0.3.0: + resolution: {integrity: sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==} + deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.) + dev: false + /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -14374,6 +17904,10 @@ packages: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} dev: false + /moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + dev: false + /mongodb-connection-string-url@3.0.0: resolution: {integrity: sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==} dependencies: @@ -14512,6 +18046,15 @@ packages: thunky: 1.1.0 dev: false + /multiparty@4.2.3: + resolution: {integrity: sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==} + engines: {node: '>= 0.10'} + dependencies: + http-errors: 1.8.1 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + dev: false + /namespace-emitter@2.0.1: resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==} dev: false @@ -14532,6 +18075,10 @@ packages: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} dev: false + /natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + dev: false + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: false @@ -14545,10 +18092,23 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: false + /netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + dev: false + /next-line@1.1.0: resolution: {integrity: sha512-+I10J3wKNoKddNxn0CNpoZ3eTZuqxjNM3b1GImVx22+ePI+Y15P8g/j3WsbP0fhzzrFzrtjOAoq5NCCucswXOQ==} dev: false + /next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + dev: false + + /nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + dev: false + /no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: @@ -14567,6 +18127,12 @@ packages: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} dev: false + /node-addon-api@1.7.2: + resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + requiresBuild: true + dev: false + optional: true + /node-addon-api@6.1.0: resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} dev: false @@ -14615,10 +18181,34 @@ packages: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: false + /node-loader@2.0.0(webpack@5.90.3): + resolution: {integrity: sha512-I5VN34NO4/5UYJaUBtkrODPWxbobrE4hgDqPrjB25yPkonFhCmZ146vTH+Zg417E9Iwoh1l/MbRs1apc5J295Q==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^5.0.0 + dependencies: + loader-utils: 2.0.4 + webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4) + dev: false + + /node-localstorage@2.2.1: + resolution: {integrity: sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==} + engines: {node: '>=0.12'} + dependencies: + write-file-atomic: 1.3.4 + dev: false + /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: false + /nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: false + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -14643,6 +18233,13 @@ packages: engines: {node: '>=10'} dev: false + /npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + dependencies: + path-key: 2.0.1 + dev: false + /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -14663,6 +18260,11 @@ packages: boolbase: 1.0.0 dev: false + /number-is-nan@1.0.1: + resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} + engines: {node: '>=0.10.0'} + dev: false + /nwsapi@2.2.7: resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} dev: false @@ -14691,6 +18293,11 @@ packages: resolution: {integrity: sha512-CsubGNxhIEChNY4cXYuA6KXafztzHqzLLZ/y3Kasf3A+sa3lL9thq3z+7o0pZqzEinjXT6lXDPAfVWI59dUyzQ==} dev: false + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: false + /object-identity-map@1.0.2: resolution: {integrity: sha512-a2XZDGyYTngvGS67kWnqVdpoaJWsY7C1GhPJvejWAFCsUioTAaiTu8oBad7c6cI4McZxr4CmvnZeycK05iav5A==} dependencies: @@ -14714,6 +18321,13 @@ packages: engines: {node: '>= 0.4'} dev: false + /object-path-immutable@3.1.1: + resolution: {integrity: sha512-NZDdPo/DEufre4+CyYZ7j6YQnWvKWnnGS+VS7dVd9M6WNcvFfnR4mW1lmakg9eCABAeE+PZW3bN/ZUTL8063IQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-plain-object: 3.0.0 + dev: false + /object.assign@4.1.5: resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} engines: {node: '>= 0.4'} @@ -14733,6 +18347,15 @@ packages: es-abstract: 1.22.4 dev: false + /object.entries@1.1.8: + resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: false + /object.fromentries@2.0.7: resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} engines: {node: '>= 0.4'} @@ -14742,6 +18365,16 @@ packages: es-abstract: 1.22.4 dev: false + /object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: false + /object.groupby@1.0.2: resolution: {integrity: sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==} dependencies: @@ -14775,14 +18408,43 @@ packages: es-abstract: 1.22.4 dev: false + /object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: false + /obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} dev: false + /octokit@3.2.1: + resolution: {integrity: sha512-u+XuSejhe3NdIvty3Jod00JvTdAE/0/+XbhIDhefHbu+2OcTRHd80aCiH6TX19ZybJmwPQBKFQmHGxp0i9mJrg==} + engines: {node: '>= 18'} + dependencies: + '@octokit/app': 14.1.0 + '@octokit/core': 5.2.0 + '@octokit/oauth-app': 6.1.0 + '@octokit/plugin-paginate-graphql': 4.0.1(@octokit/core@5.2.0) + '@octokit/plugin-paginate-rest': 11.3.1(@octokit/core@5.2.0) + '@octokit/plugin-rest-endpoint-methods': 13.2.2(@octokit/core@5.2.0) + '@octokit/plugin-retry': 6.0.1(@octokit/core@5.2.0) + '@octokit/plugin-throttling': 8.2.0(@octokit/core@5.2.0) + '@octokit/request-error': 5.1.0 + '@octokit/types': 13.5.0 + dev: false + /ohash@1.1.3: resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} dev: false + /omggif@1.0.10: + resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} + dev: false + /on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -14808,6 +18470,12 @@ packages: wrappy: 1.0.2 dev: false + /one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + dependencies: + fn.name: 1.1.0 + dev: false + /onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -14840,6 +18508,10 @@ packages: hasBin: true dev: false + /option@0.2.4: + resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==} + dev: false + /optional-js@2.3.0: resolution: {integrity: sha512-B0LLi+Vg+eko++0z/b8zIv57kp7HKEzaPJo7LowJXMUKYdf+3XJGu/cw03h/JhIOsLnP+cG5QnTHAuicjA5fMw==} dev: false @@ -14881,11 +18553,35 @@ packages: forwarded-parse: 2.1.2 dev: false + /os-locale@3.1.0: + resolution: {integrity: sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==} + engines: {node: '>=6'} + dependencies: + execa: 1.0.0 + lcid: 2.0.0 + mem: 4.3.0 + dev: false + /p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} dev: false + /p-defer@1.0.0: + resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} + engines: {node: '>=4'} + dev: false + + /p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + dev: false + + /p-is-promise@2.1.0: + resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==} + engines: {node: '>=6'} + dev: false + /p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -14977,10 +18673,42 @@ packages: engines: {node: '>=6'} dev: false + /pac-proxy-agent@7.0.2: + resolution: {integrity: sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==} + engines: {node: '>= 14'} + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.1 + debug: 4.3.5 + get-uri: 6.0.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.5 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.4 + transitivePeerDependencies: + - supports-color + dev: false + + /pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + dev: false + /pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} dev: false + /pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: false + + /pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + dev: false + /param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: @@ -14995,6 +18723,33 @@ packages: callsites: 3.1.0 dev: false + /parse-bmfont-ascii@1.0.6: + resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==} + dev: false + + /parse-bmfont-binary@1.0.6: + resolution: {integrity: sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==} + dev: false + + /parse-bmfont-xml@1.1.6: + resolution: {integrity: sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==} + dependencies: + xml-parse-from-string: 1.0.1 + xml2js: 0.5.0 + dev: false + + /parse-headers@2.0.5: + resolution: {integrity: sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==} + dev: false + + /parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + dev: false + /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -15093,6 +18848,11 @@ packages: engines: {node: '>=0.10.0'} dev: false + /path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + dev: false + /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -15136,10 +18896,33 @@ packages: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} dev: false + /pdf-lib@1.17.1: + resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==} + dependencies: + '@pdf-lib/standard-fonts': 1.0.0 + '@pdf-lib/upng': 1.0.1 + pako: 1.0.11 + tslib: 1.14.1 + dev: false + + /pdfjs-dist@2.12.313: + resolution: {integrity: sha512-1x6iXO4Qnv6Eb+YFdN5JdUzt4pAkxSp3aLAYPX93eQCyg/m7QFzXVWJHJVtoW48CI8HCXju4dSkhQZwoheL5mA==} + peerDependencies: + worker-loader: ^3.0.8 + peerDependenciesMeta: + worker-loader: + optional: true + dev: false + /peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} dev: false + /peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + dev: false + /peek-stream@1.1.3: resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} dependencies: @@ -15160,6 +18943,20 @@ packages: is-reference: 3.0.2 dev: false + /phin@2.9.3: + resolution: {integrity: sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + dev: false + + /phin@3.7.1: + resolution: {integrity: sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==} + engines: {node: '>= 8'} + dependencies: + centra: 2.7.0 + transitivePeerDependencies: + - debug + dev: false + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: false @@ -15196,6 +18993,29 @@ packages: engines: {node: '>= 6'} dev: false + /pixelmatch@4.0.2: + resolution: {integrity: sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==} + hasBin: true + dependencies: + pngjs: 3.4.0 + dev: false + + /pkg-conf@3.1.0: + resolution: {integrity: sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==} + engines: {node: '>=6'} + dependencies: + find-up: 3.0.0 + load-json-file: 5.3.0 + dev: false + + /pkg-conf@4.0.0: + resolution: {integrity: sha512-7dmgi4UY4qk+4mj5Cd8v/GExPo0K+SlY+hulOSdfZ/T6jVH6//y7NtzZo5WrfhDBxuQ0jCa7fLZmNaNh7EWL/w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + find-up: 6.3.0 + load-json-file: 7.0.1 + dev: false + /pkg-dir@3.0.0: resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} engines: {node: '>=6'} @@ -15240,12 +19060,26 @@ packages: fsevents: 2.3.2 dev: false + /plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + dependencies: + '@xmldom/xmldom': 0.8.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + dev: false + /png-chunks-extract@1.0.0: resolution: {integrity: sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==} dependencies: crc-32: 0.3.0 dev: false + /pngjs@3.4.0: + resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} + engines: {node: '>=4.0.0'} + dev: false + /polished@4.3.1: resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} engines: {node: '>=10'} @@ -15390,6 +19224,14 @@ packages: source-map-js: 1.0.2 dev: false + /posthog-js@1.122.0: + resolution: {integrity: sha512-+8R2/nLaWyI5Jp2Ly7L52qcgDFU3xryyoNG52DPJ8dlGnagphxIc0mLNGurgyKeeTGycsOsuOIP4dtofv3ZoBA==} + deprecated: This version of posthog-js is deprecated, please update posthog-js, and do not use this version! Check out our JS docs at https://posthog.com/docs/libraries/js + dependencies: + fflate: 0.4.8 + preact: 10.22.1 + dev: false + /preact@10.22.1: resolution: {integrity: sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==} dev: false @@ -15498,6 +19340,14 @@ packages: engines: {node: '>=0.4.0'} dev: false + /promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + dev: false + /prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -15506,6 +19356,14 @@ packages: sisteransi: 1.0.5 dev: false + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + dev: false + /properties@1.2.1: resolution: {integrity: sha512-qYNxyMj1JeW54i/EWEFsM1cVwxJbtgPp8+0Wg9XjNaK6VE/c4oRi6PNu5p7w1mNXEIQIjV5Wwn8v8Gz82/QzdQ==} engines: {node: '>=0.10'} @@ -15673,6 +19531,22 @@ packages: ipaddr.js: 1.9.1 dev: false + /proxy-agent@6.4.0: + resolution: {integrity: sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.1 + debug: 4.3.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + lru-cache: 7.18.3 + pac-proxy-agent: 7.0.2 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.4 + transitivePeerDependencies: + - supports-color + dev: false + /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false @@ -15712,6 +19586,10 @@ packages: engines: {node: '>=6'} dev: false + /punycode@1.3.2: + resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} + dev: false + /punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} dev: false @@ -15721,6 +19599,13 @@ packages: engines: {node: '>=6'} dev: false + /pupa@3.1.0: + resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==} + engines: {node: '>=12.20'} + dependencies: + escape-goat: 4.0.0 + dev: false + /puppeteer-core@2.1.1: resolution: {integrity: sha512-n13AWriBMPYxnpbb6bnaY5YoY6rGj8vPLrz6CZF3o0qJNEwlcfJVxBzYZ0NJsQ21UbdJoijPCDrM++SUVEz7+w==} engines: {node: '>=8.16.0'} @@ -15739,6 +19624,38 @@ packages: - supports-color dev: false + /puppeteer-core@22.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.4): + resolution: {integrity: sha512-rl4tOY5LcA3e374GAlsGGHc05HL3eGNf5rZ+uxkl6id9zVZKcwcp1Z+Nd6byb6WPiPeecT/dwz8f/iUm+AZQSw==} + engines: {node: '>=18'} + dependencies: + '@puppeteer/browsers': 2.3.0 + chromium-bidi: 0.6.2(devtools-protocol@0.0.1312386) + debug: 4.3.5 + devtools-protocol: 0.0.1312386 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /puppeteer@22.14.0(bufferutil@4.0.8)(typescript@5.3.3)(utf-8-validate@6.0.4): + resolution: {integrity: sha512-MGTR6/pM8zmWbTdazb6FKnwIihzsSEXBPH49mFFU96DNZpQOevCAZMnjBZGlZRGRzRK6aADCavR6SQtrbv5dQw==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + dependencies: + '@puppeteer/browsers': 2.3.0 + cosmiconfig: 9.0.0(typescript@5.3.3) + devtools-protocol: 0.0.1312386 + puppeteer-core: 22.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + transitivePeerDependencies: + - bufferutil + - supports-color + - typescript + - utf-8-validate + dev: false + /pure-rand@6.0.4: resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} dev: false @@ -15757,6 +19674,11 @@ packages: side-channel: 1.0.5 dev: false + /qs@6.5.3: + resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} + engines: {node: '>=0.6'} + dev: false + /query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -15767,6 +19689,12 @@ packages: strict-uri-encode: 2.0.0 dev: false + /querystring@0.2.0: + resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + dev: false + /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} dev: false @@ -15789,10 +19717,20 @@ packages: engines: {node: '>=10'} dev: false + /quick-lru@6.1.2: + resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} + engines: {node: '>=12'} + dev: false + /ramda@0.29.0: resolution: {integrity: sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==} dev: false + /random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + dev: false + /random-poly-fill@1.0.1: resolution: {integrity: sha512-bMOL0hLfrNs52+EHtIPIXxn2PxYwXb0qjnKruTjXiM/sKfYqj506aB2plFwWW1HN+ri724bAVVGparh4AtlJKw==} dev: false @@ -15848,6 +19786,10 @@ packages: scheduler: 0.23.0 dev: false + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + dev: false + /react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} dev: false @@ -15912,6 +19854,18 @@ packages: loose-envify: 1.4.0 dev: false + /read-config-file@6.3.2: + resolution: {integrity: sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==} + engines: {node: '>=12.0.0'} + dependencies: + config-file-ts: 0.2.6 + dotenv: 9.0.2 + dotenv-expand: 5.1.0 + js-yaml: 4.1.0 + json5: 2.2.3 + lazy-val: 1.0.5 + dev: false + /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -15952,6 +19906,13 @@ packages: util-deprecate: 1.0.2 dev: false + /readable-web-to-node-stream@3.0.2: + resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} + engines: {node: '>=8'} + dependencies: + readable-stream: 3.6.2 + dev: false + /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -15959,6 +19920,10 @@ packages: picomatch: 2.3.1 dev: false + /real-cancellable-promise@1.2.0: + resolution: {integrity: sha512-FYhmx1FVSgoPRjneoTjh+EKZcNb8ijl/dyatTzase5eujYhVrLNDOiIY6AgQq7GU1kOoLgEd9jLVbhFg8k8dOQ==} + dev: false + /recast@0.23.4: resolution: {integrity: sha512-qtEDqIZGVcSZCHniWwZWbRy79Dc6Wp3kT/UmDA2RJKBPg7+7k51aQBZirHmUGn5uvHf2rg8DkjizrN26k61ATw==} engines: {node: '>= 4'} @@ -15977,6 +19942,19 @@ packages: resolve: 1.22.8 dev: false + /reflect.getprototypeof@1.0.6: + resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + globalthis: 1.0.3 + which-builtin-type: 1.1.3 + dev: false + /regenerate-unicode-properties@10.1.1: resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} engines: {node: '>=4'} @@ -16067,6 +20045,10 @@ packages: unist-util-visit: 2.0.3 dev: false + /remedial@1.0.8: + resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==} + dev: false + /renderkid@3.0.0: resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} dependencies: @@ -16103,6 +20085,10 @@ packages: - supports-color dev: false + /require-main-filename@1.0.1: + resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==} + dev: false + /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: false @@ -16111,6 +20097,13 @@ packages: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} dev: false + /resolve-cwd@2.0.0: + resolution: {integrity: sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==} + engines: {node: '>=4'} + dependencies: + resolve-from: 3.0.0 + dev: false + /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -16118,6 +20111,11 @@ packages: resolve-from: 5.0.0 dev: false + /resolve-from@3.0.0: + resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} + engines: {node: '>=4'} + dev: false + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -16157,6 +20155,15 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: false + /resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: false + /responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} dependencies: @@ -16171,6 +20178,11 @@ packages: signal-exit: 3.0.7 dev: false + /retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + dev: false + /retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -16187,6 +20199,7 @@ packages: /rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true dependencies: glob: 7.2.3 @@ -16206,6 +20219,20 @@ packages: glob: 7.2.3 dev: false + /roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + requiresBuild: true + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.3 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + dev: false + optional: true + /rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} dev: false @@ -16241,6 +20268,16 @@ packages: isarray: 2.0.5 dev: false + /safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: false + /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} dev: false @@ -16258,6 +20295,11 @@ packages: is-regex: 1.1.4 dev: false + /safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + dev: false + /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: false @@ -16271,6 +20313,12 @@ packages: rimraf: 2.7.1 dev: false + /sanitize-filename@1.6.3: + resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} + dependencies: + truncate-utf8-bytes: 1.0.2 + dev: false + /sass-loader@13.3.3(sass@1.71.1)(webpack@5.90.3): resolution: {integrity: sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==} engines: {node: '>= 14.15.0'} @@ -16305,6 +20353,10 @@ packages: source-map-js: 1.0.2 dev: false + /sax@1.2.1: + resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} + dev: false + /sax@1.3.0: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} dev: false @@ -16372,6 +20424,12 @@ packages: node-forge: 1.3.1 dev: false + /semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + requiresBuild: true + dev: false + optional: true + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -16390,6 +20448,12 @@ packages: lru-cache: 6.0.0 dev: false + /semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + dev: false + /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} @@ -16409,6 +20473,15 @@ packages: statuses: 2.0.1 dev: false + /serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + type-fest: 0.13.1 + dev: false + optional: true + /serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} dependencies: @@ -16438,6 +20511,10 @@ packages: send: 0.18.0 dev: false + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: false + /set-cookie-serde@1.0.0: resolution: {integrity: sha512-Vq8e5GsupfJ7okHIvEPcfs5neCo7MZ1ZuWrO3sllYi3DOWt6bSSCpADzqXjz3k0fXehnoFIrmmhty9IN6U6BXQ==} dev: false @@ -16464,10 +20541,18 @@ packages: has-property-descriptors: 1.0.2 dev: false + /setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + dev: false + /setprototypeof@1.1.0: resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} dev: false + /setprototypeof@1.1.1: + resolution: {integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==} + dev: false + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false @@ -16498,6 +20583,13 @@ packages: tunnel-agent: 0.6.0 dev: false + /shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + dependencies: + shebang-regex: 1.0.0 + dev: false + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -16505,6 +20597,11 @@ packages: shebang-regex: 3.0.0 dev: false + /shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + dev: false + /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} @@ -16524,6 +20621,16 @@ packages: object-inspect: 1.13.1 dev: false + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + dev: false + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: false @@ -16551,6 +20658,13 @@ packages: is-arrayish: 0.3.2 dev: false + /simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + dependencies: + semver: 7.6.0 + dev: false + /simplytyped@3.3.0(typescript@5.3.3): resolution: {integrity: sha512-mz4RaNdKTZiaKXgi6P1k/cdsxV3gz+y1Wh2NXHWD40dExktLh4Xx/h6MFakmQWODZHj/2rKe59acacpL74ZhQA==} peerDependencies: @@ -16577,15 +20691,61 @@ packages: engines: {node: '>=8'} dev: false + /slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + dev: false + + /slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: false + optional: true + + /slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + dev: false + + /slide@1.1.6: + resolution: {integrity: sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==} + dev: false + /slugify@1.6.6: resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} engines: {node: '>=8.0.0'} dev: false + /smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + dev: false + /smartcrop@2.0.5: resolution: {integrity: sha512-aXoHTM8XlC51g96kgZkYxZ2mx09/ibOrIVLiUNOFozV/MHmFSgEr1/5CKVBoFD5vd+re2wSy0xra21CyjRITzA==} dev: false + /smee-client@1.2.5: + resolution: {integrity: sha512-Tv3AVvZD9sjEwBvsm1jc8to96FwmZMOYt5UL7qu3vAuA/3F/hqKe9gdRSlkvpRO4ttUAUrDKRpjM0vUnXZQhqA==} + hasBin: true + dependencies: + commander: 9.5.0 + eventsource: 2.0.2 + morgan: 1.10.0 + superagent: 8.1.2 + validator: 13.12.0 + transitivePeerDependencies: + - supports-color + dev: false + /sockjs@0.3.24: resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} dependencies: @@ -16594,6 +20754,25 @@ packages: websocket-driver: 0.7.4 dev: false + /socks-proxy-agent@8.0.4: + resolution: {integrity: sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.1 + debug: 4.3.5 + socks: 2.8.3 + transitivePeerDependencies: + - supports-color + dev: false + + /socks@2.8.3: + resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + dev: false + /sonic-boom@1.4.1: resolution: {integrity: sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==} dependencies: @@ -16611,6 +20790,20 @@ packages: sander: 0.5.1 dev: false + /sort-keys-length@1.0.1: + resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} + engines: {node: '>=0.10.0'} + dependencies: + sort-keys: 1.1.2 + dev: false + + /sort-keys@1.1.2: + resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==} + engines: {node: '>=0.10.0'} + dependencies: + is-plain-obj: 1.1.0 + dev: false + /source-list-map@2.0.1: resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} dev: false @@ -16711,6 +20904,10 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: false + /sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + dev: false + /sql-summary@1.0.1: resolution: {integrity: sha512-IpCr2tpnNkP3Jera4ncexsZUp0enJBLr+pHCyTweMUBrbJsTgQeLWx1FXLhoBj/MvcnUQpkgOn2EY8FKOkUzww==} dev: false @@ -16720,6 +20917,10 @@ packages: deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' dev: false + /stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + dev: false + /stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -16731,6 +20932,21 @@ packages: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} dev: false + /standard-engine@15.1.0: + resolution: {integrity: sha512-VHysfoyxFu/ukT+9v49d4BRXIokFRZuH3z1VRxzFArZdjSCFpro6rEIU3ji7e4AoAtuSfKBkiOmsrDqKW5ZSRw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + get-stdin: 8.0.0 + minimist: 1.2.8 + pkg-conf: 3.1.0 + xdg-basedir: 4.0.0 + dev: false + + /stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + dev: false + /statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -16846,6 +21062,23 @@ packages: strip-ansi: 6.0.1 dev: false + /string-width@1.0.2: + resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} + engines: {node: '>=0.10.0'} + dependencies: + code-point-at: 1.1.0 + is-fullwidth-code-point: 1.0.0 + strip-ansi: 3.0.1 + dev: false + + /string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + dev: false + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -16864,6 +21097,40 @@ packages: strip-ansi: 7.1.0 dev: false + /string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + dependencies: + emoji-regex: 10.3.0 + get-east-asian-width: 1.2.0 + strip-ansi: 7.1.0 + dev: false + + /string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.2 + set-function-name: 2.0.2 + side-channel: 1.0.6 + dev: false + + /string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + dependencies: + define-properties: 1.2.1 + es-abstract: 1.22.4 + dev: false + /string.prototype.trim@1.2.8: resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} engines: {node: '>= 0.4'} @@ -16873,6 +21140,16 @@ packages: es-abstract: 1.22.4 dev: false + /string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: false + /string.prototype.trimend@1.0.7: resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} dependencies: @@ -16881,6 +21158,14 @@ packages: es-abstract: 1.22.4 dev: false + /string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: false + /string.prototype.trimstart@1.0.7: resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} dependencies: @@ -16889,6 +21174,15 @@ packages: es-abstract: 1.22.4 dev: false + /string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: false + /string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: @@ -16901,6 +21195,20 @@ packages: safe-buffer: 5.2.1 dev: false + /strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + dependencies: + ansi-regex: 2.1.1 + dev: false + + /strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + dependencies: + ansi-regex: 3.0.1 + dev: false + /strip-ansi@5.2.0: resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} engines: {node: '>=6'} @@ -16932,6 +21240,11 @@ packages: engines: {node: '>=8'} dev: false + /strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + dev: false + /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -16963,6 +21276,18 @@ packages: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} dev: false + /strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + dev: false + + /stubborn-fs@1.2.5: + resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} + dev: false + /style-loader@3.3.4(webpack@5.90.3): resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} engines: {node: '>= 12.13.0'} @@ -16972,6 +21297,34 @@ packages: webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4) dev: false + /sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + dependencies: + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + dev: false + + /superagent@8.1.2: + resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} + engines: {node: '>=6.4.0 <13 || >=14'} + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.3.4 + fast-safe-stringify: 2.1.1 + form-data: 4.0.0 + formidable: 2.1.2 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.11.2 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + dev: false + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -17231,6 +21584,16 @@ packages: bare-path: 2.1.0 dev: false + /tar-fs@3.0.6: + resolution: {integrity: sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==} + dependencies: + pump: 3.0.0 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 2.1.5 + bare-path: 2.1.0 + dev: false + /tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -17262,6 +21625,28 @@ packages: yallist: 4.0.0 dev: false + /telegram@2.22.2: + resolution: {integrity: sha512-9payizc801Aqqu4eTGPc0huxKnIwFe0hn18mrpbgAKKiMLurX/UIQW/fjPhI5ONzockDFsaXDFqTreXBoG1u3A==} + dependencies: + '@cryptography/aes': 0.1.1 + async-mutex: 0.3.2 + big-integer: 1.6.52 + buffer: 6.0.3 + htmlparser2: 6.1.0 + mime: 3.0.0 + node-localstorage: 2.2.1 + pako: 2.1.0 + path-browserify: 1.0.1 + real-cancellable-promise: 1.2.0 + socks: 2.8.3 + store2: 2.14.3 + ts-custom-error: 3.3.1 + websocket: 1.0.35 + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + dev: false + /telejson@6.0.8: resolution: {integrity: sha512-nerNXi+j8NK1QEfBHtZUN/aLdDcyupA//9kAboYLrtzZlPLpUfqbVGWb9zz91f/mIjRbAYhbgtnJHY8I1b5MBg==} dependencies: @@ -17286,6 +21671,13 @@ packages: engines: {node: '>=8'} dev: false + /temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + dev: false + /temp@0.8.4: resolution: {integrity: sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==} engines: {node: '>=6.0.0'} @@ -17375,6 +21767,10 @@ packages: minimatch: 3.1.2 dev: false + /text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + dev: false + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: false @@ -17397,20 +21793,47 @@ packages: readable-stream: 3.6.2 dev: false + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + dev: false + /thunky@1.1.0: resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} dev: false + /timm@1.7.1: + resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==} + dev: false + /tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} dev: false + /tiny-typed-emitter@2.1.0: + resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} + dev: false + + /tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + dev: false + /tippy.js@6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} dependencies: '@popperjs/core': 2.11.8 dev: false + /tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + dependencies: + tmp: 0.2.3 + dev: false + + /tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + dev: false + /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} dev: false @@ -17437,11 +21860,24 @@ packages: resolution: {integrity: sha512-kE5wyCQJ40hqUaRVkyQ4z5+4juzYsv/eK+aqD97N62YH0TxFhzJvo22RUQQZdO3YnXAk42ZOfOpjVdy+Z0YokA==} dev: false + /toidentifier@1.0.0: + resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==} + engines: {node: '>=0.6'} + dev: false + /toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} dev: false + /token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + dev: false + /toposort@2.0.2: resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} dev: false @@ -17490,6 +21926,22 @@ packages: engines: {node: '>= 0.4'} dev: false + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: false + + /triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + dev: false + + /truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + dependencies: + utf8-byte-length: 1.0.5 + dev: false + /ts-api-utils@1.2.1(typescript@5.3.3): resolution: {integrity: sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==} engines: {node: '>=16'} @@ -17499,6 +21951,11 @@ packages: typescript: 5.3.3 dev: false + /ts-custom-error@3.3.1: + resolution: {integrity: sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==} + engines: {node: '>=14.0.0'} + dev: false + /ts-debounce@4.0.0: resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==} dev: false @@ -17508,6 +21965,12 @@ packages: engines: {node: '>=6.10'} dev: false + /ts-invariant@0.4.4: + resolution: {integrity: sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==} + dependencies: + tslib: 1.14.1 + dev: false + /ts-jest@29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3): resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -17558,6 +22021,34 @@ packages: webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4) dev: false + /ts-node-dev@2.0.0(@types/node@20.11.19)(typescript@5.3.3): + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} + engines: {node: '>=0.8.0'} + hasBin: true + peerDependencies: + node-notifier: '*' + typescript: '*' + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + chokidar: 3.6.0 + dynamic-dedupe: 0.3.0 + minimist: 1.2.8 + mkdirp: 1.0.4 + resolve: 1.22.8 + rimraf: 2.7.1 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) + tsconfig: 7.0.0 + typescript: 5.3.3 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + dev: false + /ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -17589,6 +22080,45 @@ packages: yn: 3.1.1 dev: false + /ts-node@7.0.1: + resolution: {integrity: sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==} + engines: {node: '>=4.2.0'} + hasBin: true + dependencies: + arrify: 1.0.1 + buffer-from: 1.1.2 + diff: 3.5.0 + make-error: 1.3.6 + minimist: 1.2.8 + mkdirp: 0.5.6 + source-map-support: 0.5.21 + yn: 2.0.0 + dev: false + + /ts-standard@12.0.2(typescript@5.3.3): + resolution: {integrity: sha512-XX2wrB9fKKTfBj4yD3ABm9iShzZcS2iWcPK8XzlBvuL20+wMiLgiz/k5tXgZwTaYq5wRhbks1Y9PelhujF/9ag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + peerDependencies: + typescript: '*' + dependencies: + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-jsx: 11.0.0(eslint-plugin-react@7.35.0)(eslint@8.56.0) + eslint-config-standard-with-typescript: 23.0.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + eslint-plugin-react: 7.35.0(eslint@8.56.0) + minimist: 1.2.8 + pkg-conf: 4.0.0 + standard-engine: 15.1.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false + /tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} dependencies: @@ -17598,6 +22128,15 @@ packages: strip-bom: 3.0.0 dev: false + /tsconfig@7.0.0: + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + dependencies: + '@types/strip-bom': 3.0.0 + '@types/strip-json-comments': 0.0.30 + strip-bom: 3.0.0 + strip-json-comments: 2.0.1 + dev: false + /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: false @@ -17611,6 +22150,16 @@ packages: engines: {node: '>=0.6.x'} dev: false + /tsutils@3.21.0(typescript@5.3.3): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 5.3.3 + dev: false + /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: @@ -17629,6 +22178,13 @@ packages: engines: {node: '>=4'} dev: false + /type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + requiresBuild: true + dev: false + optional: true + /type-fest@0.16.0: resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} engines: {node: '>=10'} @@ -17644,6 +22200,11 @@ packages: engines: {node: '>=10'} dev: false + /type-fest@0.3.1: + resolution: {integrity: sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==} + engines: {node: '>=6'} + dev: false + /type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -17659,6 +22220,11 @@ packages: engines: {node: '>=12.20'} dev: false + /type-fest@4.23.0: + resolution: {integrity: sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==} + engines: {node: '>=16'} + dev: false + /type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -17667,6 +22233,10 @@ packages: mime-types: 2.1.35 dev: false + /type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + dev: false + /typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} @@ -17711,12 +22281,30 @@ packages: possible-typed-array-names: 1.0.0 dev: false + /typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + dev: false + /typed-emitter@2.1.0: resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==} optionalDependencies: rxjs: 7.8.1 dev: false + /typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + dependencies: + is-typedarray: 1.0.0 + dev: false + /typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} dev: false @@ -17743,10 +22331,22 @@ packages: dev: false optional: true + /uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + dependencies: + random-bytes: 1.0.0 + dev: false + /uid2@0.0.4: resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} dev: false + /uint8array-extras@1.4.0: + resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} + engines: {node: '>=18'} + dev: false + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -17756,6 +22356,17 @@ packages: which-boxed-primitive: 1.0.2 dev: false + /unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + dependencies: + buffer: 5.7.1 + through: 2.3.8 + dev: false + + /underscore@1.13.7: + resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} + dev: false + /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: false @@ -17820,6 +22431,22 @@ packages: unist-util-visit-parents: 3.1.1 dev: false + /universal-github-app-jwt@1.1.2: + resolution: {integrity: sha512-t1iB2FmLFE+yyJY9+3wMx0ejB+MQpEVkH0gQv7dR6FZyltyq+ZZO0uDpbopxhrZ3SLEO4dCEkIujOMldEQ2iOA==} + dependencies: + '@types/jsonwebtoken': 9.0.6 + jsonwebtoken: 9.0.2 + dev: false + + /universal-user-agent@6.0.1: + resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + dev: false + + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: false + /universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -17849,6 +22476,14 @@ packages: engines: {node: '>=8'} dev: false + /unused-filename@4.0.1: + resolution: {integrity: sha512-ZX6U1J04K1FoSUeoX1OicAhw4d0aro2qo+L8RhJkiGTNtBNkd/Fi1Wxoc9HzcVu6HfOzm0si/N15JjxFmD1z6A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + escape-string-regexp: 5.0.0 + path-exists: 5.0.0 + dev: false + /update-browserslist-db@1.0.13(browserslist@4.21.5): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true @@ -17901,6 +22536,17 @@ packages: requires-port: 1.0.0 dev: false + /url-template@2.0.8: + resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + dev: false + + /url@0.10.3: + resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} + dependencies: + punycode: 1.3.2 + querystring: 0.2.0 + dev: false + /url@0.11.3: resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==} dependencies: @@ -17908,6 +22554,10 @@ packages: qs: 6.11.2 dev: false + /urlpattern-polyfill@10.0.0: + resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + dev: false + /use-callback-ref@1.3.1(react@18.2.0): resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==} engines: {node: '>=10'} @@ -17948,6 +22598,14 @@ packages: tslib: 2.6.2 dev: false + /utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + requiresBuild: true + dependencies: + node-gyp-build: 4.8.0 + dev: false + /utf-8-validate@6.0.4: resolution: {integrity: sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==} engines: {node: '>=6.14.2'} @@ -17956,6 +22614,16 @@ packages: node-gyp-build: 4.8.0 dev: false + /utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + dev: false + + /utif@2.0.1: + resolution: {integrity: sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==} + dependencies: + pako: 1.0.11 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false @@ -17979,6 +22647,17 @@ packages: engines: {node: '>= 0.4.0'} dev: false + /uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: false + + /uuid@8.0.0: + resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} + hasBin: true + dev: false + /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -18013,11 +22692,27 @@ packages: spdx-expression-parse: 3.0.1 dev: false + /validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + dev: false + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} dev: false + /verror@1.10.1: + resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} + engines: {node: '>=0.6.0'} + requiresBuild: true + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.4.1 + dev: false + optional: true + /w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} dev: false @@ -18367,6 +23062,18 @@ packages: engines: {node: '>=0.8.0'} dev: false + /websocket@1.0.35: + resolution: {integrity: sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==} + engines: {node: '>=4.0.0'} + dependencies: + bufferutil: 4.0.8 + debug: 2.6.9 + es5-ext: 0.10.64 + typedarray-to-buffer: 3.1.5 + utf-8-validate: 5.0.10 + yaeti: 0.0.6 + dev: false + /whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} @@ -18402,6 +23109,10 @@ packages: webidl-conversions: 3.0.1 dev: false + /when-exit@2.1.3: + resolution: {integrity: sha512-uVieSTccFIr/SFQdFWN/fFaQYmV37OKtuaGphMAzi4DmmUlrvRBJW5WSLkHyjNQY/ePJMz3LoiX9R3yy1Su6Hw==} + dev: false + /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: @@ -18412,6 +23123,24 @@ packages: is-symbol: 1.0.4 dev: false + /which-builtin-type@1.1.3: + resolution: {integrity: sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==} + engines: {node: '>= 0.4'} + dependencies: + function.prototype.name: 1.1.6 + has-tostringtag: 1.0.2 + is-async-function: 2.0.0 + is-date-object: 1.0.5 + is-finalizationregistry: 1.0.2 + is-generator-function: 1.0.10 + is-regex: 1.1.4 + is-weakref: 1.0.2 + isarray: 2.0.5 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.1 + which-typed-array: 1.1.14 + dev: false + /which-collection@1.0.1: resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} dependencies: @@ -18421,6 +23150,10 @@ packages: is-weakset: 2.0.2 dev: false + /which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: false + /which-typed-array@1.1.14: resolution: {integrity: sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==} engines: {node: '>= 0.4'} @@ -18432,6 +23165,24 @@ packages: has-tostringtag: 1.0.2 dev: false + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: false + + /which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -18448,10 +23199,57 @@ packages: resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} dev: false + /winston-daily-rotate-file@5.0.0(winston@3.13.1): + resolution: {integrity: sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==} + engines: {node: '>=8'} + peerDependencies: + winston: ^3 + dependencies: + file-stream-rotator: 0.6.1 + object-hash: 3.0.0 + triple-beam: 1.4.1 + winston: 3.13.1 + winston-transport: 4.7.1 + dev: false + + /winston-transport@4.7.1: + resolution: {integrity: sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==} + engines: {node: '>= 12.0.0'} + dependencies: + logform: 2.6.1 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + dev: false + + /winston@3.13.1: + resolution: {integrity: sha512-SvZit7VFNvXRzbqGHsv5KSmgbEYR5EiQfDAL9gxYkRqa934Hnk++zze0wANKtMHcy/gI4W/3xmSDwlhf865WGw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.5 + is-stream: 2.0.1 + logform: 2.6.1 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.4.3 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.7.1 + dev: false + /wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} dev: false + /wrap-ansi@2.1.0: + resolution: {integrity: sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==} + engines: {node: '>=0.10.0'} + dependencies: + string-width: 1.0.2 + strip-ansi: 3.0.1 + dev: false + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -18474,6 +23272,14 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: false + /write-file-atomic@1.3.4: + resolution: {integrity: sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==} + dependencies: + graceful-fs: 4.2.11 + imurmurhash: 0.1.4 + slide: 1.1.6 + dev: false + /write-file-atomic@2.4.3: resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} dependencies: @@ -18528,11 +23334,29 @@ packages: utf-8-validate: 6.0.4 dev: false + /xdg-basedir@4.0.0: + resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} + engines: {node: '>=8'} + dev: false + + /xhr@2.6.0: + resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==} + dependencies: + global: 4.4.0 + is-function: 1.0.2 + parse-headers: 2.0.5 + xtend: 4.0.2 + dev: false + /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} dev: false + /xml-parse-from-string@1.0.1: + resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} + dev: false + /xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} engines: {node: '>=4.0.0'} @@ -18541,11 +23365,30 @@ packages: xmlbuilder: 11.0.1 dev: false + /xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.3.0 + xmlbuilder: 11.0.1 + dev: false + + /xmlbuilder@10.1.1: + resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} + engines: {node: '>=4.0'} + dev: false + /xmlbuilder@11.0.1: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} dev: false + /xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + requiresBuild: true + dev: false + /xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: false @@ -18581,11 +23424,20 @@ packages: yjs: 13.6.12 dev: false + /y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: false + /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} dev: false + /yaeti@0.0.6: + resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==} + engines: {node: '>=0.10.32'} + dev: false + /yallist@2.1.2: resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} dev: false @@ -18608,11 +23460,35 @@ packages: engines: {node: '>= 14'} dev: false + /yargs-parser@11.1.1: + resolution: {integrity: sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: false + /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} dev: false + /yargs@12.0.5: + resolution: {integrity: sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==} + dependencies: + cliui: 4.1.0 + decamelize: 1.2.0 + find-up: 3.0.0 + get-caller-file: 1.0.3 + os-locale: 3.1.0 + require-directory: 2.1.1 + require-main-filename: 1.0.1 + set-blocking: 2.0.0 + string-width: 2.1.1 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 11.1.1 + dev: false + /yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -18645,6 +23521,11 @@ packages: engines: {node: '>= 4.0.0'} dev: false + /yn@2.0.0: + resolution: {integrity: sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ==} + engines: {node: '>=4'} + dev: false + /yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -18667,6 +23548,21 @@ packages: css-what: 6.1.0 dev: false + /zen-observable-ts@0.8.21: + resolution: {integrity: sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==} + dependencies: + tslib: 1.14.1 + zen-observable: 0.8.15 + dev: false + + /zen-observable@0.8.15: + resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} + dev: false + + /zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + dev: false + file:projects/account-service.tgz: resolution: {integrity: sha512-m2wnBUxpiATGM4uSuNprCebldYmLYweixWs/lkbmn2oxzV797WaTrigIQEEPowYOydBskozS7uc/PgBj9WhkCw==, tarball: file:projects/account-service.tgz} name: '@rush-temp/account-service' @@ -18869,12 +23765,111 @@ packages: - ts-node dev: false + file:projects/analytics-collector-assets.tgz(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2): + resolution: {integrity: sha512-ThEhCV2dJchrcM0mfUjNFdD0/qOzl3qC7glianWUl4Qsj03IrYyjbxvdsV7Fy/49UhQ+YUt46X39kgIWVBnWiw==, tarball: file:projects/analytics-collector-assets.tgz} + id: file:projects/analytics-collector-assets.tgz + name: '@rush-temp/analytics-collector-assets' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12) + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - svelte + - ts-node + dev: false + + file:projects/analytics-collector.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2): + resolution: {integrity: sha512-m298Tce6R1UfZPYO43qimcjCqQNDKFQ08UiLhIcwIfk5Tg+Gm67q9Da79dE2fKDxKqIoAIPytneAgtOOXIMVaw==, tarball: file:projects/analytics-collector.tgz} + id: file:projects/analytics-collector.tgz + name: '@rush-temp/analytics-collector' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12) + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@types/node' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - svelte + - ts-node + dev: false + + file:projects/analytics-service.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-3hpPEwojBH/WARd3evdL3zOnOb9ygsuGRlKmPARpwJx3BV2iI/yapLa2vF7pZUSOZRNK+U3sfYNUGmjMUiV3PA==, tarball: file:projects/analytics-service.tgz} + id: file:projects/analytics-service.tgz + name: '@rush-temp/analytics-service' + version: 0.0.0 + dependencies: + '@sentry/node': 7.118.0 + '@types/jest': 29.5.12 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + simplytyped: 3.3.0(typescript@5.3.3) + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + winston: 3.13.1 + winston-daily-rotate-file: 5.0.0(winston@3.13.1) + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@types/node' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + dev: false + file:projects/analytics.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): resolution: {integrity: sha512-wLu52RMlBePatFSm958iRCQIa02H20WkOWGqfFKnErgHL66ZnRXmOLQ5/yvKRpIXkFIiFeLkraYKIofUQQ+VxA==, tarball: file:projects/analytics.tgz} id: file:projects/analytics.tgz name: '@rush-temp/analytics' version: 0.0.0 dependencies: + '@sentry/node': 7.118.0 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) @@ -18889,6 +23884,8 @@ packages: simplytyped: 3.3.0(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) typescript: 5.3.3 + winston: 3.13.1 + winston-daily-rotate-file: 5.0.0(winston@3.13.1) transitivePeerDependencies: - '@babel/core' - '@jest/types' @@ -20018,6 +25015,215 @@ packages: - ts-node dev: false + file:projects/desktop-1.tgz(webpack@5.90.3): + resolution: {integrity: sha512-Fkk5uNa4NwlCVU5yJUf4X3FcGljXj0qUH7iyoCEAk8EKQ/Mi3OfG/KqK03kEeuM1KP8T1CtArtUhWDsM66/AFQ==, tarball: file:projects/desktop-1.tgz} + id: file:projects/desktop-1.tgz + name: '@rush-temp/desktop-1' + version: 0.0.0 + dependencies: + '@electron/notarize': 2.3.2 + '@types/node': 20.11.19 + '@vercel/webpack-asset-relocator-loader': 1.7.4 + cross-env: 7.0.3 + dotenv: 16.0.3 + electron: 31.3.0 + electron-builder: 24.13.3 + electron-squirrel-startup: 1.0.1 + node-loader: 2.0.0(webpack@5.90.3) + typescript: 5.3.3 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + - webpack + dev: false + + file:projects/desktop-preferences-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-V9FjBfsoqyYAdA87p3TLstkOi39YScp9wSs7CdTzvcFIPGLPG6sOPZ8QQuAXbGxGjgfOcxWuqxkh5hllNIfyHQ==, tarball: file:projects/desktop-preferences-assets.tgz} + id: file:projects/desktop-preferences-assets.tgz + name: '@rush-temp/desktop-preferences-assets' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + dev: false + + file:projects/desktop-preferences-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): + resolution: {integrity: sha512-wwqD1uJ8gqfv5OaLGnnxkWEsEE77053PgEzs15EQkrk4KM3Kvuy25XW6JwXi0xD1e4BOcqc4FhEYiTpIvtW2Rw==, tarball: file:projects/desktop-preferences-resources.tgz} + id: file:projects/desktop-preferences-resources.tgz + name: '@rush-temp/desktop-preferences-resources' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12) + sass: 1.71.1 + svelte: 4.2.12 + svelte-check: 3.6.9(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12) + svelte-eslint-parser: 0.33.1(svelte@4.2.12) + svelte-loader: 3.2.0(svelte@4.2.12) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)(typescript@5.3.3) + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@types/node' + - babel-jest + - babel-plugin-macros + - coffeescript + - esbuild + - less + - node-notifier + - postcss + - postcss-load-config + - pug + - stylus + - sugarss + - supports-color + - ts-node + dev: false + + file:projects/desktop-preferences.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-r57UCjwBTP/+jAYOyYhTX9dEnn9OJHgp5zVSCh3DRwuWd/Em036njORfRZOxy6CFNFgpqdWbEhiah6GR4dM5Ow==, tarball: file:projects/desktop-preferences.tgz} + id: file:projects/desktop-preferences.tgz + name: '@rush-temp/desktop-preferences' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@types/node' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + dev: false + + file:projects/desktop.tgz(bufferutil@4.0.8)(sass@1.71.1)(utf-8-validate@6.0.4): + resolution: {integrity: sha512-2edgvVz9CadWKo8x9QaO8UNcvjxSGilnSzeLzkV9ijegvq0HinqBqu4Q/sFmKKHMBXYc5ZNW3wJ+/2BGvEIMZw==, tarball: file:projects/desktop.tgz} + id: file:projects/desktop.tgz + name: '@rush-temp/desktop' + version: 0.0.0 + dependencies: + '@types/node': 20.11.19 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + '@vercel/webpack-asset-relocator-loader': 1.7.4 + autoprefixer: 10.4.17(postcss@8.4.35) + browserslist: 4.21.5 + commander: 8.3.0 + compression-webpack-plugin: 10.0.0(webpack@5.90.3) + copy-webpack-plugin: 11.0.0(webpack@5.90.3) + cross-env: 7.0.3 + css-loader: 5.2.7(webpack@5.90.3) + dotenv: 16.0.3 + dotenv-webpack: 8.0.1(webpack@5.90.3) + electron: 31.3.0 + electron-context-menu: 4.0.1 + electron-log: 5.1.7 + electron-squirrel-startup: 1.0.1 + electron-store: 10.0.0 + electron-updater: 6.2.1 + electron-windows-badge: 1.1.0 + esbuild: 0.20.1 + esbuild-loader: 4.0.3(webpack@5.90.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + file-loader: 6.2.0(webpack@5.90.3) + fork-ts-checker-webpack-plugin: 7.3.0(typescript@5.3.3)(webpack@5.90.3) + html-webpack-plugin: 5.6.0(webpack@5.90.3) + livekit-client: 2.2.0 + mini-css-extract-plugin: 2.8.0(webpack@5.90.3) + node-loader: 2.0.0(webpack@5.90.3) + postcss: 8.4.35 + postcss-load-config: 4.0.2(postcss@8.4.35)(ts-node@10.9.2) + postcss-loader: 7.3.4(postcss@8.4.35)(typescript@5.3.3)(webpack@5.90.3) + prettier: 3.2.5 + sass-loader: 13.3.3(sass@1.71.1)(webpack@5.90.3) + style-loader: 3.3.4(webpack@5.90.3) + svelte: 4.2.12 + svelte-loader: 3.2.0(svelte@4.2.12) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)(typescript@5.3.3) + svgo-loader: 3.0.3 + ts-loader: 9.5.1(typescript@5.3.3)(webpack@5.90.3) + ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) + ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3) + typescript: 5.3.3 + update-browserslist-db: 1.0.13(browserslist@4.21.5) + webpack: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4) + webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) + webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.15.1)(webpack@5.90.3) + webpack-dev-server: 4.15.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)(webpack-cli@5.1.4)(webpack@5.90.3) + transitivePeerDependencies: + - '@babel/core' + - '@rspack/core' + - '@swc/core' + - '@swc/wasm' + - '@webpack-cli/generators' + - bufferutil + - coffeescript + - debug + - fibers + - less + - node-notifier + - node-sass + - pug + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - uglify-js + - utf-8-validate + - vue-template-compiler + dev: false + file:projects/devmodel-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): resolution: {integrity: sha512-EM4KBZ9lTqifDM/zjXmvtXAQV9nokDucxG2kaw6sGe0eW8w2k4NDcVggvHMHUbydTPBmWuvZNpPvZ8RxgrBPog==, tarball: file:projects/devmodel-resources.tgz} id: file:projects/devmodel-resources.tgz @@ -20094,6 +25300,114 @@ packages: - ts-node dev: false + file:projects/diffview-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-3Dai2sbD262yh84ni7cWX71PIW/fO/fiMEOXxqpRSsTf0SyVQ2BIYyOXxkwOL8MgRA5g5s9rb8qHW0CjZVuPXw==, tarball: file:projects/diffview-assets.tgz} + id: file:projects/diffview-assets.tgz + name: '@rush-temp/diffview-assets' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + dev: false + + file:projects/diffview-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): + resolution: {integrity: sha512-fi0UUvQTrAAt70IEF8CC7O6h5UfknnXO0xHylpL/Wn7h6oOQHlfPl9VhAx0PJW6TDvw3CltEE35pVSLyDml1CA==, tarball: file:projects/diffview-resources.tgz} + id: file:projects/diffview-resources.tgz + name: '@rush-temp/diffview-resources' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + diff2html: 3.4.48 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2) + fast-equals: 5.0.1 + highlight.js: 11.8.0 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12) + sass: 1.71.1 + svelte: 4.2.12 + svelte-check: 3.6.9(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12) + svelte-eslint-parser: 0.33.1(svelte@4.2.12) + svelte-loader: 3.2.0(svelte@4.2.12) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)(typescript@5.3.3) + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@types/node' + - babel-jest + - babel-plugin-macros + - coffeescript + - esbuild + - less + - node-notifier + - postcss + - postcss-load-config + - pug + - stylus + - sugarss + - supports-color + - ts-node + dev: false + + file:projects/diffview.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-bP063eugaDREOI+bDy18zm4gnEg6L7JOhO0MkvA8YVdgspInuvMdOnKBzbZ2aM0WUkhrE/XLgONtQ89r61/cBg==, tarball: file:projects/diffview.tgz} + id: file:projects/diffview.tgz + name: '@rush-temp/diffview' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@types/node' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + dev: false + file:projects/document-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2): resolution: {integrity: sha512-rCP4MYljBKhtWZRUDLUYu/3NEni8PhLhAEHjoHj9fjQyzQdQc6b4ElYxGA4Nwja9bZGluBwiinEw2L4xviw9Rw==, tarball: file:projects/document-assets.tgz} id: file:projects/document-assets.tgz @@ -20398,6 +25712,113 @@ packages: - supports-color dev: false + file:projects/github-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-cnPTqSnJuFmZO+tMdYFpXJiEzCdPRGbT/wh6PzHNJzjvO74+lvT0PMy2aqEqvL78aPYvrqrfHEw7pXpHTWY2EA==, tarball: file:projects/github-assets.tgz} + id: file:projects/github-assets.tgz + name: '@rush-temp/github-assets' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + dev: false + + file:projects/github-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): + resolution: {integrity: sha512-U0e6zVcDD10tlw75iRfrQae7mRGgM7hNNH87NwWThWmk5HIVH8nS6iltVT97afp3G+YwmMDpuDvC1SO59PdL5w==, tarball: file:projects/github-resources.tgz} + id: file:projects/github-resources.tgz + name: '@rush-temp/github-resources' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@types/qs': 6.9.11 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2) + fast-equals: 5.0.1 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12) + sass: 1.71.1 + svelte: 4.2.12 + svelte-check: 3.6.9(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12) + svelte-eslint-parser: 0.33.1(svelte@4.2.12) + svelte-loader: 3.2.0(svelte@4.2.12) + svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)(typescript@5.3.3) + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@types/node' + - babel-jest + - babel-plugin-macros + - coffeescript + - esbuild + - less + - node-notifier + - postcss + - postcss-load-config + - pug + - stylus + - sugarss + - supports-color + - ts-node + dev: false + + file:projects/github.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-hQ+gXG22OPXE3ef1Kup271oOrBkBBT5EqjMNTi8vRAFJIMOnOUygtRaCJwozvvZ2evab/LgnHmF2qt8eIsdKBQ==, tarball: file:projects/github.tgz} + id: file:projects/github.tgz + name: '@rush-temp/github' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@types/node' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + dev: false + file:projects/gmail-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2): resolution: {integrity: sha512-9W7yiTwmPezJiUw26f3EelOKyS7NUvTSCsg2ohuRipbosMpzwPxL/4UlikZJ8Wx5nPtDiDuC/dFU12V1fDLHzQ==, tarball: file:projects/gmail-assets.tgz} id: file:projects/gmail-assets.tgz @@ -21366,7 +26787,7 @@ packages: dev: false file:projects/model-all.tgz: - resolution: {integrity: sha512-UJzQ4TlmbHmPJfDhn7VNn01VuXSeWITV8e1pq3mssAshd1F0ElQiDJYMtCtJcUVQnGWwx7r4sRCqDL3P3Qdayg==, tarball: file:projects/model-all.tgz} + resolution: {integrity: sha512-iZAri5sHzRIutIt2bdrG1zI/QAu/lt1KC14lDRvHKLCaEtThF4RLlBqYHQ/rZM8aZyHIiGLwVztxvzR8D5EnZA==, tarball: file:projects/model-all.tgz} name: '@rush-temp/model-all' version: 0.0.0 dependencies: @@ -21388,6 +26809,24 @@ packages: - supports-color dev: false + file:projects/model-analytics-collector.tgz: + resolution: {integrity: sha512-OcczFEesVXSXUuJ/HpZ880slAZeV8Y/MhZArlxBlO90Tm3jO42cyQLAi0kO36ZAkO/gGVpAHUpuZT0QB70ovvw==, tarball: file:projects/model-analytics-collector.tgz} + name: '@rush-temp/model-analytics-collector' + version: 0.0.0 + dependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + prettier: 3.2.5 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false + file:projects/model-attachment.tgz: resolution: {integrity: sha512-l6ido0KZWoQdDjZCYPzyauy2JZk04kXh48IpVSb4t4UwPfRXQKdKNZFecJxEyIkXW5T2uFYsWSpg+w7jYx85hw==, tarball: file:projects/model-attachment.tgz} name: '@rush-temp/model-attachment' @@ -21542,6 +26981,24 @@ packages: - supports-color dev: false + file:projects/model-desktop-preferences.tgz: + resolution: {integrity: sha512-nU19Bq6EblQcApwcv6mwIVv3/9Eb7NlipPdmmBdxA90VqJAmMy2alC0G8ZPe22AgrUhvE1INxkjaEm2EyIHyaw==, tarball: file:projects/model-desktop-preferences.tgz} + name: '@rush-temp/model-desktop-preferences' + version: 0.0.0 + dependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + prettier: 3.2.5 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false + file:projects/model-document.tgz: resolution: {integrity: sha512-aQ1dqQdlY2Lc/HpchhCuHa3+9hqLhLA/I1OyVFBd1ap+qHLlLo5PyNnQE/laSmgW/3ULKXMG1xm3jRcmTT4VdA==, tarball: file:projects/model-document.tgz} name: '@rush-temp/model-document' @@ -21579,6 +27036,24 @@ packages: - supports-color dev: false + file:projects/model-github.tgz: + resolution: {integrity: sha512-WUEiwmSsxVx9D70sRcSs3b27dvPyA4KeocITd2vwxuDqFqYo22GIjLP1AUpMTQxmR6hxNV/9Nn7Hus7M8KnKNg==, tarball: file:projects/model-github.tgz} + name: '@rush-temp/model-github' + version: 0.0.0 + dependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + prettier: 3.2.5 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false + file:projects/model-gmail.tgz: resolution: {integrity: sha512-FoDVnyY59pxmZGAethkhK1+8i3yA/HAtbB1dfRUB2VBSRrqtj44pUsBGFEMqvseVyj9F1DGGJEIMmr6XSRM6Ug==, tarball: file:projects/model-gmail.tgz} name: '@rush-temp/model-gmail' @@ -22561,13 +28036,11 @@ packages: - supports-color dev: false - file:projects/model-text-editor.tgz(@tiptap/pm@2.2.4): - resolution: {integrity: sha512-a2Q7SUya+W86b84tBEmHhf0+LJNVxtfweKCuwzGh77gfmSz8ok0aQjpRA61JHQuk8NfP96K+CKAA4oJh1LMpAg==, tarball: file:projects/model-text-editor.tgz} - id: file:projects/model-text-editor.tgz + file:projects/model-text-editor.tgz: + resolution: {integrity: sha512-z1TlVHF8VxcMeChSZ70ZGJsKdGSKisnpnPGuLmp2yYS30ckD5ohoj4KeqgbKu+qT+72FYSg4COXFoAYnszgfoA==, tarball: file:projects/model-text-editor.tgz} name: '@rush-temp/model-text-editor' version: 0.0.0 dependencies: - '@tiptap/starter-kit': 2.2.4(@tiptap/pm@2.2.4) '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) eslint: 8.56.0 @@ -22578,7 +28051,6 @@ packages: prettier: 3.2.5 typescript: 5.3.3 transitivePeerDependencies: - - '@tiptap/pm' - supports-color dev: false @@ -23024,7 +28496,7 @@ packages: dev: false file:projects/pod-account.tgz: - resolution: {integrity: sha512-8TNm2djgcpy9V0WMdb8XmOwbMxGSSpvVs76DMoKhEcAjFHtmK/G32IOOvh721krtM5NibFqpEo4SERxjYWn7Qg==, tarball: file:projects/pod-account.tgz} + resolution: {integrity: sha512-yI2LfUu2SCsh/b97mSszloCkfnaZ0s0AllwwszhB5S3V6jJFzAcyhfX2Zw9y2XRbe2UOPMyISs16JrCFDI8Rog==, tarball: file:projects/pod-account.tgz} name: '@rush-temp/pod-account' version: 0.0.0 dependencies: @@ -23072,8 +28544,52 @@ packages: - supports-color dev: false + file:projects/pod-analytics-collector.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4): + resolution: {integrity: sha512-Sx9mTOCsgDTcoC8h9VXeemVHerKLMDzdW4aJ08fdsF9UA35tLWmbDXw+fn3gPUNj2l+QT5ExEF9rV1Iez1FIbw==, tarball: file:projects/pod-analytics-collector.tgz} + id: file:projects/pod-analytics-collector.tgz + name: '@rush-temp/pod-analytics-collector' + version: 0.0.0 + dependencies: + '@tsconfig/node16': 1.0.4 + '@types/cors': 2.8.17 + '@types/express': 4.17.21 + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@types/ws': 8.5.11 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + cors: 2.8.5 + dotenv: 16.0.3 + esbuild: 0.20.1 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-node: 11.1.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + express: 4.19.2 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + puppeteer: 22.14.0(bufferutil@4.0.8)(typescript@5.3.3)(utf-8-validate@6.0.4) + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) + typescript: 5.3.3 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - bufferutil + - node-notifier + - supports-color + - utf-8-validate + dev: false + file:projects/pod-backup.tgz: - resolution: {integrity: sha512-cHCcTxI/Vi0V6XlrDbRHoYPHkyxXMt46iCY6sAlIuSU/6pgTQbNfZvrwCMVvlJS6AbZ/aCtmKsw6B54x9hnPQQ==, tarball: file:projects/pod-backup.tgz} + resolution: {integrity: sha512-kDZfttPxTuMHKyNcRqOBDuzJjR+H4DGGAFor6GvK4vNDRDHmw0vxFg8zkhDCSLgUAGaHIq/Xw8SxkADO1BKJQQ==, tarball: file:projects/pod-backup.tgz} name: '@rush-temp/pod-backup' version: 0.0.0 dependencies: @@ -23108,8 +28624,69 @@ packages: - supports-color dev: false + file:projects/pod-calendar.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4): + resolution: {integrity: sha512-a3IO+LV4RLUrLCIwlzK6HfwU0q4QkepgRaQi4/ybG+He0Oqy/eWvZnHHXry0FjArSrZjr2QOnxOlYsipXv/zfg==, tarball: file:projects/pod-calendar.tgz} + id: file:projects/pod-calendar.tgz + name: '@rush-temp/pod-calendar' + version: 0.0.0 + dependencies: + '@tsconfig/node16': 1.0.4 + '@types/cors': 2.8.17 + '@types/express': 4.17.21 + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@types/node-fetch': 2.6.11 + '@types/ws': 8.5.11 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + cors: 2.8.5 + dotenv: 16.0.3 + esbuild: 0.20.1 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-node: 11.1.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + express: 4.19.2 + fast-equals: 5.0.1 + file-api: 0.10.4 + form-data: 4.0.0 + google-auth-library: 8.9.0 + googleapis: 122.0.0 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + jwt-simple: 0.5.6 + mongodb: 6.8.0 + node-fetch: 2.7.0 + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3) + typescript: 5.3.3 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@babel/core' + - '@jest/types' + - '@mongodb-js/zstd' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - bufferutil + - encoding + - gcp-metadata + - kerberos + - mongodb-client-encryption + - node-notifier + - snappy + - socks + - supports-color + - ts-node + - utf-8-validate + dev: false + file:projects/pod-collaborator.tgz: - resolution: {integrity: sha512-6bWC34OX31zOMwh9oOxnxkFD0sq7GCmCk5VZDUVsmjY7hbLsFSdNQ3mpKk+5nFwrIsxwtHZtghqYow9isy8Bbg==, tarball: file:projects/pod-collaborator.tgz} + resolution: {integrity: sha512-3qz7k9xtE0tZjr0MlSs062eI+X4WfPh+anTCA7btPShvsC+5GdSYBXxD5nCT4rO7klt7pCLfXMozSEJrIXTM2Q==, tarball: file:projects/pod-collaborator.tgz} name: '@rush-temp/pod-collaborator' version: 0.0.0 dependencies: @@ -23142,7 +28719,7 @@ packages: dev: false file:projects/pod-front.tgz: - resolution: {integrity: sha512-TjmoiIgWSRZLN6LJ4JAkk5dQ2kkF8WFEw36PAsNJPILc09xtKsxiJSsWwrn78epTB6jk2ugx4P8VuFnAERPvaA==, tarball: file:projects/pod-front.tgz} + resolution: {integrity: sha512-dfeO6R4HILQ4+Z+sBAH3KAPWLOXtwCQjlaxfD9//EOAyiXBynRyGKJUgudkWm/iG0fpgfxdYogYzg2OyUjKtJg==, tarball: file:projects/pod-front.tgz} name: '@rush-temp/pod-front' version: 0.0.0 dependencies: @@ -23188,8 +28765,250 @@ packages: - supports-color dev: false + file:projects/pod-github.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-prosemirror@1.2.2): + resolution: {integrity: sha512-WnIupxKJkkE/0F06plCcJxaijJf8mwN80v8tgb1hqkpNOB2BbBWdcDbFk0apeiASq7SiI22r16l7aZA2kAqjig==, tarball: file:projects/pod-github.tgz} + id: file:projects/pod-github.tgz + name: '@rush-temp/pod-github' + version: 0.0.0 + dependencies: + '@octokit/types': 12.6.0 + '@octokit/webhooks': 12.2.0 + '@octokit/webhooks-types': 7.5.1 + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) + '@tiptap/extension-bubble-menu': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-code': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-code-block': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-collaboration': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4)(y-prosemirror@1.2.2) + '@tiptap/extension-collaboration-cursor': 2.2.4(@tiptap/core@2.2.4)(y-prosemirror@1.2.2) + '@tiptap/extension-gapcursor': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-heading': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-highlight': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-link': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-mention': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4)(@tiptap/suggestion@2.2.4) + '@tiptap/extension-placeholder': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-table': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-table-cell': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-table-header': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-table-row': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-task-item': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-task-list': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-typography': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/html': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/pm': 2.2.4 + '@tiptap/starter-kit': 2.2.4(@tiptap/pm@2.2.4) + '@tiptap/suggestion': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@types/body-parser': 1.19.5 + '@types/compression': 1.7.5 + '@types/cors': 2.8.17 + '@types/express': 4.17.21 + '@types/express-fileupload': 1.4.4 + '@types/jest': 29.5.12 + '@types/markdown-it': 13.0.8 + '@types/node': 20.11.19 + '@types/uuid': 8.3.4 + '@types/ws': 8.5.11 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + body-parser: 1.20.2 + cors: 2.8.5 + cross-env: 7.0.3 + dotenv: 16.0.3 + esbuild: 0.20.1 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + express: 4.19.2 + fast-equals: 5.0.1 + graphql: 16.9.0 + graphql-binding: 2.5.2 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + markdown-it: 14.0.0 + mongodb: 6.8.0 + octokit: 3.2.1 + prettier: 3.2.5 + smee-client: 1.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) + typescript: 5.3.3 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@babel/core' + - '@jest/types' + - '@mongodb-js/zstd' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - bufferutil + - gcp-metadata + - kerberos + - mongodb-client-encryption + - node-notifier + - snappy + - socks + - supports-color + - utf-8-validate + - y-prosemirror + dev: false + + file:projects/pod-gmail.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4): + resolution: {integrity: sha512-EgPrNV5SyE8PVkKfTc30asvvzcTaDEZnQ1krUOi8rIzQcgLuPIRgMwTc6yuqu3SAzC2zPrwudTMqzDPpwJTRxw==, tarball: file:projects/pod-gmail.tgz} + id: file:projects/pod-gmail.tgz + name: '@rush-temp/pod-gmail' + version: 0.0.0 + dependencies: + '@tsconfig/node16': 1.0.4 + '@types/cors': 2.8.17 + '@types/express': 4.17.21 + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@types/node-fetch': 2.6.11 + '@types/ws': 8.5.11 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + cors: 2.8.5 + dotenv: 16.0.3 + esbuild: 0.20.1 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-node: 11.1.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + express: 4.19.2 + fast-equals: 5.0.1 + file-api: 0.10.4 + form-data: 4.0.0 + gaxios: 5.1.3 + google-auth-library: 8.9.0 + googleapis: 122.0.0 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + jwt-simple: 0.5.6 + mongodb: 6.8.0 + node-fetch: 2.7.0 + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3) + typescript: 5.3.3 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@babel/core' + - '@jest/types' + - '@mongodb-js/zstd' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - bufferutil + - encoding + - gcp-metadata + - kerberos + - mongodb-client-encryption + - node-notifier + - snappy + - socks + - supports-color + - ts-node + - utf-8-validate + dev: false + + file:projects/pod-love.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4): + resolution: {integrity: sha512-h64U5de7eDYWI5zbLFQtUEArWz+OVeQ3BWtCd2yy9Yw3uzgPOE+etmib6KtcC0MEcDSl3ZNN0UOrV1IDQzy+nQ==, tarball: file:projects/pod-love.tgz} + id: file:projects/pod-love.tgz + name: '@rush-temp/pod-love' + version: 0.0.0 + dependencies: + '@tsconfig/node16': 1.0.4 + '@types/cors': 2.8.17 + '@types/express': 4.17.21 + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@types/uuid': 8.3.4 + '@types/ws': 8.5.11 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + cors: 2.8.5 + dotenv: 16.0.3 + esbuild: 0.20.1 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-node: 11.1.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + express: 4.19.2 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + jwt-simple: 0.5.6 + livekit-server-sdk: 2.6.0 + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) + typescript: 5.3.3 + uuid: 8.3.2 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - bufferutil + - node-notifier + - supports-color + - utf-8-validate + dev: false + + file:projects/pod-print.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4): + resolution: {integrity: sha512-R5/pYQyA+eel7D9xg8BETAPFIpV0RbV2XsrhAlSMnljb7p1PORRK9lOaAVR/YGvsG3E2YCkyT7czM3jKXzrBog==, tarball: file:projects/pod-print.tgz} + id: file:projects/pod-print.tgz + name: '@rush-temp/pod-print' + version: 0.0.0 + dependencies: + '@tsconfig/node16': 1.0.4 + '@types/cors': 2.8.17 + '@types/express': 4.17.21 + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@types/ws': 8.5.11 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + cors: 2.8.5 + dotenv: 16.0.3 + esbuild: 0.20.1 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-node: 11.1.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + express: 4.19.2 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + mammoth: 1.8.0 + prettier: 3.2.5 + puppeteer: 22.14.0(bufferutil@4.0.8)(typescript@5.3.3)(utf-8-validate@6.0.4) + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - bufferutil + - node-notifier + - supports-color + - utf-8-validate + dev: false + file:projects/pod-server.tgz: - resolution: {integrity: sha512-XoIVqsoRTp9S4Pn4XFHZJODjWt0EtAp/yhFw5QkINxMOaAN6Wr7uiFuXngkbe52JQbvx2pJ39WNnEXwMi5s40w==, tarball: file:projects/pod-server.tgz} + resolution: {integrity: sha512-SKYUU6rZD72X+TPRSMr2uaiNrrFE686lm0Ds5ZTObKaPflbXfOua6hR//f4i7YstbNeNMwCTm73OSfkqLsTOXA==, tarball: file:projects/pod-server.tgz} name: '@rush-temp/pod-server' version: 0.0.0 dependencies: @@ -23202,6 +29021,7 @@ packages: elastic-apm-node: 3.26.0 esbuild: 0.20.1 eslint: 8.56.0 + eslint-config-prettier: 8.10.0(eslint@8.56.0) eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) eslint-plugin-import: 2.29.1(eslint@8.56.0) eslint-plugin-n: 15.7.0(eslint@8.56.0) @@ -23223,6 +29043,168 @@ packages: - supports-color dev: false + file:projects/pod-ses.tgz: + resolution: {integrity: sha512-4NK99adA3u9ZU6yOeB+H9gavGw+/B+S0+Qkb1ubRQ4t1YUC7oeVTub7A1PUVAf3CyqAjp7lBTwwtGBXwyYpTxw==, tarball: file:projects/pod-ses.tgz} + name: '@rush-temp/pod-ses' + version: 0.0.0 + dependencies: + '@tsconfig/node16': 1.0.4 + '@types/cors': 2.8.17 + '@types/express': 4.17.21 + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@types/ws': 8.5.11 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + aws-sdk: 2.1664.0 + cors: 2.8.5 + cross-env: 7.0.3 + dotenv: 16.0.3 + esbuild: 0.20.1 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-node: 11.1.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + express: 4.19.2 + got: 11.8.6 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - node-notifier + - supports-color + dev: false + + file:projects/pod-sign.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4): + resolution: {integrity: sha512-kceqIYAazdwqnkcouhx4BZzUIJ1cxvYas4XtukNGHEB1Sc+mK+mag390ljMcM6fSHSZz9Ix1ke0OZGmNqbCTCQ==, tarball: file:projects/pod-sign.tgz} + id: file:projects/pod-sign.tgz + name: '@rush-temp/pod-sign' + version: 0.0.0 + dependencies: + '@signpdf/placeholder-pdf-lib': 3.2.4(pdf-lib@1.17.1) + '@signpdf/signer-p12': 3.2.4(node-forge@1.3.1) + '@signpdf/signpdf': 3.2.4 + '@signpdf/utils': 3.2.4 + '@tsconfig/node16': 1.0.4 + '@types/cors': 2.8.17 + '@types/express': 4.17.21 + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@types/ws': 8.5.11 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + cors: 2.8.5 + dotenv: 16.0.3 + esbuild: 0.20.1 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-node: 11.1.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + express: 4.19.2 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + mongodb: 6.8.0 + node-forge: 1.3.1 + pdf-lib: 1.17.1 + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) + typescript: 5.3.3 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@babel/core' + - '@jest/types' + - '@mongodb-js/zstd' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - bufferutil + - gcp-metadata + - kerberos + - mongodb-client-encryption + - node-notifier + - snappy + - socks + - supports-color + - utf-8-validate + dev: false + + file:projects/pod-telegram.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4): + resolution: {integrity: sha512-qt/UwzvrmgaVI2yI5vdGPHaghv5JF+ysgUY9GwwarkzqlZNBScnUo262CYNWy26iPdhVqNbvGYPrAgVecGHBFg==, tarball: file:projects/pod-telegram.tgz} + id: file:projects/pod-telegram.tgz + name: '@rush-temp/pod-telegram' + version: 0.0.0 + dependencies: + '@tsconfig/node16': 1.0.4 + '@types/cors': 2.8.17 + '@types/express': 4.17.21 + '@types/jest': 29.5.12 + '@types/mime': 3.0.4 + '@types/node': 20.11.19 + '@types/node-fetch': 2.6.11 + '@types/ws': 8.5.11 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + big-integer: 1.6.52 + bson: 6.8.0 + cors: 2.8.5 + dotenv: 16.0.3 + esbuild: 0.20.1 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + express: 4.19.2 + form-data: 4.0.0 + htmlparser2: 9.1.0 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + jwt-simple: 0.5.6 + mime: 3.0.0 + mongodb: 6.8.0 + node-fetch: 2.7.0 + prettier: 3.2.5 + telegram: 2.22.2 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3) + ts-standard: 12.0.2(typescript@5.3.3) + typescript: 5.3.3 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@babel/core' + - '@jest/types' + - '@mongodb-js/zstd' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - bufferutil + - encoding + - gcp-metadata + - kerberos + - mongodb-client-encryption + - node-notifier + - snappy + - socks + - supports-color + - ts-node + - utf-8-validate + dev: false + file:projects/preference-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2): resolution: {integrity: sha512-VlBSKBg3XmuMLtxNAS703aS+dhhb5a7H5Ns2nzhhv7w3KlAqtwp6cQ5VLxceNuRaPbTtI+2K+YkjFb2S1ld5VQ==, tarball: file:projects/preference-assets.tgz} id: file:projects/preference-assets.tgz @@ -23443,11 +29425,12 @@ packages: dev: false file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.71.1)(ts-node@10.9.2)(utf-8-validate@6.0.4): - resolution: {integrity: sha512-ALcGhikSF7a0YF6j81P1X3YX9B7x+Pu+wCS5mNa4YgcJEod+A7uGhAMcqBXHUsYsBvbJ1aymocOD88ACDDWEUQ==, tarball: file:projects/prod.tgz} + resolution: {integrity: sha512-nTK4PWGg4h8wB67Ax8fO8iqUg1fOr8JhCgNTuz2o6VqTFnk7cDEc2k9FuYkdSho6HelRqEFcKGkmXsZEAungXQ==, tarball: file:projects/prod.tgz} id: file:projects/prod.tgz name: '@rush-temp/prod' version: 0.0.0 dependencies: + '@sentry/svelte': 7.101.1(svelte@4.2.12) '@types/node': 20.11.19 autoprefixer: 10.4.17(postcss@8.4.35) browserslist: 4.21.5 @@ -23464,6 +29447,7 @@ packages: postcss: 8.4.35 postcss-load-config: 4.0.2(postcss@8.4.35)(ts-node@10.9.2) postcss-loader: 7.3.4(postcss@8.4.35)(typescript@5.3.3)(webpack@5.90.3) + posthog-js: 1.122.0 sass-loader: 13.3.3(sass@1.71.1)(webpack@5.90.3) style-loader: 3.3.4(webpack@5.90.3) svelte: 4.2.12 @@ -23609,6 +29593,51 @@ packages: - ts-node dev: false + file:projects/qms-doc-import-tool.tgz: + resolution: {integrity: sha512-s2EDYV09exzo01lbGDlzMq9D0cx2OILHS8bqQCgntVVWEK9nQypMd4gi/nCbmVhyHRapQih7D81Nf6zuSUohJg==, tarball: file:projects/qms-doc-import-tool.tgz} + name: '@rush-temp/qms-doc-import-tool' + version: 0.0.0 + dependencies: + '@types/domhandler': 2.4.5 + '@types/htmlparser2': 3.10.7 + '@types/jest': 29.5.12 + '@types/minio': 7.0.18 + '@types/node': 20.11.19 + '@types/node-fetch': 2.6.11 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + commander: 8.3.0 + cross-env: 7.0.3 + domhandler: 5.0.3 + domutils: 3.1.0 + esbuild: 0.20.1 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + form-data: 4.0.0 + htmlparser2: 9.1.0 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + mammoth: 1.8.0 + node-fetch: 2.7.0 + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) + typescript: 5.3.3 + zod: 3.23.8 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - encoding + - node-notifier + - supports-color + dev: false + file:projects/qms-tests-sanity.tgz: resolution: {integrity: sha512-2NJnLTGNRdrMXybXslRQNNCyyDLmsA069UyXn8uBG/WQaMHzr9rTXvtRpltXSfgCn+WAc8VidyT68O6J8RAPQA==, tarball: file:projects/qms-tests-sanity.tgz} name: '@rush-temp/qms-tests-sanity' @@ -23913,6 +29942,69 @@ packages: - ts-node dev: false + file:projects/rekoni-service.tgz(webpack@5.90.3): + resolution: {integrity: sha512-gL/GzTK+Tojt7wj5A5jeMnbmSQBiEdZnnp5h0h2zpIirV6mjQ0kM1kSLuFAA938dx72yGh21+ZNnH5uj12/70A==, tarball: file:projects/rekoni-service.tgz} + id: file:projects/rekoni-service.tgz + name: '@rush-temp/rekoni-service' + version: 0.0.0 + dependencies: + '@anticrm/skillset': 0.6.0 + '@types/body-parser': 1.19.5 + '@types/cors': 2.8.17 + '@types/email-addresses': 3.0.0 + '@types/express': 4.17.21 + '@types/express-form-data': 2.0.5 + '@types/html-to-text': 8.1.1 + '@types/jest': 29.5.12 + '@types/jimp': 0.2.28 + '@types/mime-types': 2.1.4 + '@types/node': 20.11.19 + '@types/pdfjs-dist': 2.10.378 + '@types/sharp': 0.32.0 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + body-parser: 1.20.2 + cors: 2.8.5 + dotenv: 16.0.3 + email-addresses: 5.0.0 + esbuild: 0.20.1 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-node: 11.1.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + express: 4.19.2 + express-form-data: 2.0.23 + html-to-text: 9.0.5 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + jimp: 0.16.13 + jwt-simple: 0.5.6 + libphonenumber-js: 1.10.56 + mammoth: 1.8.0 + mime-types: 2.1.35 + node-loader: 2.0.0(webpack@5.90.3) + pdfjs-dist: 2.12.313 + prettier: 3.2.5 + sharp: 0.32.6 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3) + ts-node-dev: 2.0.0(@types/node@20.11.19)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - debug + - node-notifier + - supports-color + - webpack + - worker-loader + dev: false + file:projects/rekoni.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): resolution: {integrity: sha512-fAbU5FuiRiyTcpY+UYcTTAbwEqZxsetdvk992xSzJ5xePHrIlzZbX3aVcqJCEFm3OBx6yXZH0Wq33RVQImHxhA==, tarball: file:projects/rekoni.tgz} id: file:projects/rekoni.tgz @@ -24180,6 +30272,66 @@ packages: - ts-node dev: false + file:projects/server-analytics-collector-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-cLqoyrE7bZmjUeQSenTvg0S2XdEUUulUTMYvYephOUR5orzG71xy3Z+HgV92YapcZMzi9RVdaataHwYhxTRDPw==, tarball: file:projects/server-analytics-collector-resources.tgz} + id: file:projects/server-analytics-collector-resources.tgz + name: '@rush-temp/server-analytics-collector-resources' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@types/node' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + dev: false + + file:projects/server-analytics-collector.tgz(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-RjYH7HxF3sqneNJUW4a/+PgKne3vojOGq8gPZBC7K0HFQY7EJkC3Y1xeFs9eTJLOYk94zCb5ktlQDAJ2H38FIQ==, tarball: file:projects/server-analytics-collector.tgz} + id: file:projects/server-analytics-collector.tgz + name: '@rush-temp/server-analytics-collector' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + dev: false + file:projects/server-attachment-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): resolution: {integrity: sha512-RWTp9dIA34k7jSIcNEGnbZzTB9xV8jnozdIkL1cTbAoxgLX/6l2wYLm/prR3CsOOB5mJwWfGoFPNE0Do1QE45w==, tarball: file:projects/server-attachment-resources.tgz} id: file:projects/server-attachment-resources.tgz @@ -24781,6 +30933,84 @@ packages: - ts-node dev: false + file:projects/server-github-model.tgz: + resolution: {integrity: sha512-1qQ8IXhyHSiV5P+M7sGq0QHkkAG2n2DK3knDQJv/xzOQamJ6kcr2nlaqtAD48LfO1wVTtUkjwYwE9RbAEgpl5w==, tarball: file:projects/server-github-model.tgz} + name: '@rush-temp/server-github-model' + version: 0.0.0 + dependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + prettier: 3.2.5 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: false + + file:projects/server-github-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-SJP95etRPH37ut50Ds6WZk6AUf6+TebCdwZJ3jqlqkCmB0Vhr1cW5cH25vJwPWMSoU0x/TlLV6NDmXZQfONbYw==, tarball: file:projects/server-github-resources.tgz} + id: file:projects/server-github-resources.tgz + name: '@rush-temp/server-github-resources' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@types/node' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + dev: false + + file:projects/server-github.tgz(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-u2n1TQZlNrcvTLjVzfYfHNn63IjChpiu3dvpcsSrkLyFgQCMOtVA7PSpMDOoe9IwpKAzz6VFsHukF5eCYpAkWw==, tarball: file:projects/server-github.tgz} + id: file:projects/server-github.tgz + name: '@rush-temp/server-github' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + dev: false + file:projects/server-gmail-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): resolution: {integrity: sha512-jc5tLhz7YJs4Nw0dwPgOpZnZA+wMstl6Vt3BuL5BOE8iMTeUTZP1fKJXf6pGVeZAGEaYzayHM9Jmw+S45/l9MQ==, tarball: file:projects/server-gmail-resources.tgz} id: file:projects/server-gmail-resources.tgz diff --git a/desktop-package/.env b/desktop-package/.env new file mode 100644 index 0000000000..ab09a13b80 --- /dev/null +++ b/desktop-package/.env @@ -0,0 +1,3 @@ +# APPLE_ID=team-member-apple-id +# APPLE_ID_APP_PASS=team-members-app-pass +# TEAM_ID=apple-dev-team-id \ No newline at end of file diff --git a/desktop-package/config/rig.json b/desktop-package/config/rig.json new file mode 100644 index 0000000000..b58354496a --- /dev/null +++ b/desktop-package/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "package" +} diff --git a/desktop-package/entitlements.mac.plist b/desktop-package/entitlements.mac.plist new file mode 100644 index 0000000000..7eb1b07f18 --- /dev/null +++ b/desktop-package/entitlements.mac.plist @@ -0,0 +1,14 @@ + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-jit + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + + diff --git a/desktop-package/package.json b/desktop-package/package.json new file mode 100644 index 0000000000..4285b1a0b5 --- /dev/null +++ b/desktop-package/package.json @@ -0,0 +1,97 @@ +{ + "name": "desktop", + "version": "0.6.266", + "main": "dist/main/electron.js", + "author": "Hardcore Engineering ", + "template": "@hcengineering/default-package", + "scripts": { + "_phase:package": "rushx package", + "package": "rushx bump && rm -rf ./dist && cp -r ../desktop/dist . && cp ../desktop/.env ./dist && echo 'done'", + "dist": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false NODE_ENV=production MODEL_VERSION=$(node ../common/scripts/show_version.js) electron-builder build -p onTag", + "dist-local": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false NODE_ENV=production MODEL_VERSION=$(node ../common/scripts/show_version.js) electron-builder build", + "dist-signed": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=true NODE_ENV=production MODEL_VERSION=$(node ../common/scripts/show_version.js) electron-builder build -c.afterSign=scripts/notarize.js -p onTag", + "dist-mac-win": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false NODE_ENV=production MODEL_VERSION=$(node ../common/scripts/show_version.js) electron-builder build --windows --x64", + "format": "echo done", + "bump": "bump-package-version" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@hcengineering/desktop": "^0.6.0", + "@vercel/webpack-asset-relocator-loader": "^1.7.3", + "node-loader": "~2.0.0", + "cross-env": "~7.0.3", + "typescript": "^5.3.3", + "electron": "^31.3.0", + "@types/node": "~20.11.16", + "electron-builder": "^24.13.3", + "@electron/notarize": "^2.3.2" + }, + "dependencies": { + "electron-squirrel-startup": "~1.0.0", + "dotenv": "~16.0.0" + }, + "homepage": "https://huly.io/", + "productName": "Huly Desktop", + "description": "Huly Desktop experience", + "build": { + "productName": "Huly", + "appId": "hc.hcengineering.Huly", + "directories": { + "output": "deploy" + }, + "files": [ + "./dist/**" + ], + "mac": { + "category": "public.app-category.utilities", + "artifactName": "Huly-macos-${version}-${arch}.${ext}", + "target": "default", + "icon": "./src/AppIcon.icns", + "hardenedRuntime": true, + "entitlements": "./entitlements.mac.plist", + "entitlementsInherit": "./entitlements.mac.plist", + "notarize": false, + "publish": { + "provider": "s3", + "bucket": "desktop-distro", + "endpoint": "https://47fadbaa4ecbea9f3e8b7043a4584e27.r2.cloudflarestorage.com", + "channel": "latest" + } + }, + "win": { + "target": [ + "zip", + "nsis" + ], + "artifactName": "Huly-windows-${version}.${ext}", + "verifyUpdateCodeSignature": false, + "icon": "./src/AppIcon.png", + "publish": { + "provider": "s3", + "bucket": "desktop-distro", + "endpoint": "https://47fadbaa4ecbea9f3e8b7043a4584e27.r2.cloudflarestorage.com", + "channel": "latest" + } + }, + "linux": { + "artifactName": "Huly-linux-${version}.${ext}", + "target": [ + "AppImage", + "deb", + "zip" + ], + "category": "Utility", + "publish": { + "provider": "s3", + "bucket": "desktop-distro", + "endpoint": "https://47fadbaa4ecbea9f3e8b7043a4584e27.r2.cloudflarestorage.com", + "channel": "latest" + } + } + }, + "keywords": [ + "electron", + "typescript", + "svelte" + ] +} diff --git a/desktop-package/readme.md b/desktop-package/readme.md new file mode 100644 index 0000000000..39f8a7cd00 --- /dev/null +++ b/desktop-package/readme.md @@ -0,0 +1,9 @@ +## Automatomatic updates + +All builds are published to R2 storage bucket avaialble at https://dist.huly.io + +To check the latest auto-updatable distributions see + +* MacOS: https://dist.huly.io/latest-mac.yml +* Linux: https://dist.huly.io/latest-linux.yml +* Windows: https://dist.huly.io/latest.yml diff --git a/desktop-package/scripts/notarize.js b/desktop-package/scripts/notarize.js new file mode 100644 index 0000000000..a39a9b8fd6 --- /dev/null +++ b/desktop-package/scripts/notarize.js @@ -0,0 +1,19 @@ +require('dotenv').config(); +const { notarize } = require('@electron/notarize'); + +exports.default = async function notarizing(context) { + const { electronPlatformName, appOutDir } = context; + if (electronPlatformName !== 'darwin') { + return; + } + + const appName = context.packager.appInfo.productFilename; + + console.log('Starting custom notarization process...') + return await notarize({ + appPath: `${appOutDir}/${appName}.app`, + appleId: process.env.APPLE_ID, + appleIdPassword: process.env.APPLE_ID_APP_PASS, + teamId: process.env.TEAM_ID + }); +}; diff --git a/desktop-package/scripts/publish-version.sh b/desktop-package/scripts/publish-version.sh new file mode 100755 index 0000000000..06fabf74b4 --- /dev/null +++ b/desktop-package/scripts/publish-version.sh @@ -0,0 +1,12 @@ +#!/bin/bash +endpoint=https://47fadbaa4ecbea9f3e8b7043a4584e27.r2.cloudflarestorage.com +bucket=desktop-distro +rawVersion=$(node common/scripts/show_version.js) +version=${rawVersion:1:${#rawVersion}-2} + +cd desktop-package/deploy +aws s3api put-object --endpoint $endpoint --bucket $bucket --key ${version}-mac.yml --body latest-mac.yml --acl public-read 2>&1 > /dev/null +aws s3api put-object --endpoint $endpoint --bucket $bucket --key ${version}-linux.yml --body latest-linux.yml --acl public-read 2>&1 > /dev/null +aws s3api put-object --endpoint $endpoint --bucket $bucket --key ${version}.yml --body latest.yml --acl public-read 2>&1 > /dev/null + +echo Successully published version ${version} to ${endpoint}/${bucket} diff --git a/desktop-package/src/AppIcon.icns b/desktop-package/src/AppIcon.icns new file mode 100644 index 0000000000..4ade8c3e54 Binary files /dev/null and b/desktop-package/src/AppIcon.icns differ diff --git a/desktop-package/src/AppIcon.ico b/desktop-package/src/AppIcon.ico new file mode 100644 index 0000000000..20b8646e33 Binary files /dev/null and b/desktop-package/src/AppIcon.ico differ diff --git a/desktop-package/src/AppIcon.png b/desktop-package/src/AppIcon.png new file mode 100644 index 0000000000..e61b0bf112 Binary files /dev/null and b/desktop-package/src/AppIcon.png differ diff --git a/desktop/.env b/desktop/.env new file mode 100644 index 0000000000..a9d1aef2c4 --- /dev/null +++ b/desktop/.env @@ -0,0 +1,2 @@ +FRONT_URL=https://huly.app +CONFIG_URL=config.json diff --git a/desktop/.env-dev b/desktop/.env-dev new file mode 100644 index 0000000000..037febc783 --- /dev/null +++ b/desktop/.env-dev @@ -0,0 +1,2 @@ +FRONT_URL=http://localhost:8087 +CONFIG_URL=config.json diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/desktop/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/desktop/config/rig.json b/desktop/config/rig.json new file mode 100644 index 0000000000..b58354496a --- /dev/null +++ b/desktop/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "package" +} diff --git a/desktop/contextBridge.svg b/desktop/contextBridge.svg new file mode 100644 index 0000000000..fba626b61f --- /dev/null +++ b/desktop/contextBridge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/desktop/declarations.d.ts b/desktop/declarations.d.ts new file mode 100644 index 0000000000..e0d241be42 --- /dev/null +++ b/desktop/declarations.d.ts @@ -0,0 +1 @@ +declare module "electron-windows-badge" \ No newline at end of file diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000000..707f7d7f5b --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,218 @@ +{ + "name": "@hcengineering/desktop", + "version": "0.6.266", + "main": "dist/main/electron.js", + "template": "@hcengineering/webpack-package", + "scripts": { + "build": "compile", + "build:watch": "tsc", + "_phase:package": "rushx package", + "_phase:validate": "compile validate", + "package": "rushx bump && cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) NODE_ENV=production webpack --stats-error-details && echo 'done'", + "webpack": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) NODE_ENV=development webpack --stats-error-details --progress -w", + "devp": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) NODE_ENV=production CLIENT_TYPE=dev webpack --progress -w", + "dev": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) NODE_ENV=development webpack --progress -w", + "start": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) NODE_ENV=production electron .", + "start-dev": "cross-env MODEL_VERSION=$(node ../common/scripts/show_version.js) NODE_ENV=development electron .", + "format": "format", + "bump": "bump-package-version" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@vercel/webpack-asset-relocator-loader": "^1.7.3", + "node-loader": "~2.0.0", + "cross-env": "~7.0.3", + "webpack-cli": "^5.0.1", + "webpack": "^5.75.0", + "mini-css-extract-plugin": "^2.2.0", + "dotenv-webpack": "^8.0.1", + "ts-loader": "^9.2.5", + "svelte-loader": "^3.2.0", + "css-loader": "^5.2.1", + "webpack-dev-server": "^4.11.1", + "style-loader": "^3.3.1", + "file-loader": "^6.2.0", + "sass-loader": "^13.2.0", + "webpack-bundle-analyzer": "^4.7.0", + "svgo-loader": "^3.0.0", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.20", + "postcss-loader": "^7.0.2", + "postcss-load-config": "^4.0.1", + "compression-webpack-plugin": "^10.0.0", + "html-webpack-plugin": "^5.5.0", + "fork-ts-checker-webpack-plugin": "~7.3.0", + "update-browserslist-db": "~1.0.11", + "browserslist": "4.21.5", + "typescript": "^5.3.3", + "ts-node": "^10.8.0", + "ts-node-dev": "^2.0.0", + "electron": "^31.3.0", + "@types/node": "~20.11.16", + "copy-webpack-plugin": "^11.0.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "eslint": "^8.54.0", + "prettier": "^3.1.0", + "esbuild": "^0.20.0", + "esbuild-loader": "^4.0.3", + "svelte-preprocess": "^5.1.3" + }, + "dependencies": { + "@hcengineering/platform": "^0.6.11", + "@hcengineering/ui": "^0.6.15", + "@hcengineering/theme": "^0.6.5", + "@hcengineering/login": "^0.6.12", + "@hcengineering/login-assets": "^0.6.0", + "@hcengineering/login-resources": "^0.6.2", + "@hcengineering/client": "^0.6.18", + "@hcengineering/workbench": "^0.6.16", + "@hcengineering/workbench-resources": "^0.6.1", + "@hcengineering/view": "^0.6.13", + "@hcengineering/view-assets": "^0.6.11", + "@hcengineering/view-resources": "^0.6.0", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/contact-resources": "^0.6.0", + "@hcengineering/task": "^0.6.20", + "@hcengineering/task-assets": "^0.6.19", + "@hcengineering/task-resources": "^0.6.0", + "@hcengineering/chunter": "^0.6.20", + "@hcengineering/chunter-assets": "^0.6.18", + "@hcengineering/chunter-resources": "^0.6.0", + "@hcengineering/recruit": "^0.6.29", + "@hcengineering/recruit-assets": "^0.6.23", + "@hcengineering/recruit-resources": "^0.6.0", + "@hcengineering/setting": "^0.6.17", + "@hcengineering/setting-assets": "^0.6.15", + "@hcengineering/setting-resources": "^0.6.0", + "@hcengineering/client-resources": "^0.6.27", + "@hcengineering/contact-assets": "^0.6.13", + "@hcengineering/activity": "^0.6.0", + "@hcengineering/activity-assets": "^0.6.3", + "@hcengineering/activity-resources": "^0.6.1", + "@hcengineering/telegram": "^0.6.21", + "@hcengineering/telegram-assets": "^0.6.0", + "@hcengineering/telegram-resources": "^0.6.0", + "@hcengineering/workbench-assets": "^0.6.14", + "@hcengineering/attachment": "^0.6.14", + "@hcengineering/attachment-assets": "^0.6.11", + "@hcengineering/attachment-resources": "^0.6.0", + "@hcengineering/lead": "^0.6.0", + "@hcengineering/lead-assets": "^0.6.0", + "@hcengineering/lead-resources": "^0.6.0", + "@hcengineering/gmail": "^0.6.22", + "@hcengineering/gmail-assets": "^0.6.0", + "@hcengineering/gmail-resources": "^0.6.0", + "@hcengineering/image-cropper": "^0.6.0", + "@hcengineering/image-cropper-resources": "^0.6.0", + "@hcengineering/inventory": "^0.6.11", + "@hcengineering/inventory-assets": "^0.6.11", + "@hcengineering/inventory-resources": "^0.6.0", + "@hcengineering/templates": "^0.6.11", + "@hcengineering/templates-assets": "^0.6.11", + "@hcengineering/templates-resources": "^0.6.0", + "@hcengineering/notification": "^0.6.23", + "@hcengineering/notification-assets": "^0.6.17", + "@hcengineering/notification-resources": "^0.6.0", + "@hcengineering/preference": "^0.6.13", + "@hcengineering/preference-assets": "^0.6.0", + "@hcengineering/core": "^0.6.32", + "@hcengineering/rekoni": "^0.6.0", + "@hcengineering/tags-assets": "^0.6.0", + "@hcengineering/tags": "^0.6.16", + "@hcengineering/tags-resources": "^0.6.0", + "@hcengineering/calendar": "^0.6.24", + "@hcengineering/calendar-assets": "^0.6.22", + "@hcengineering/calendar-resources": "^0.6.0", + "@hcengineering/presentation": "^0.6.3", + "@hcengineering/tracker": "^0.6.24", + "@hcengineering/tracker-assets": "^0.6.0", + "@hcengineering/tracker-resources": "^0.6.0", + "@hcengineering/text-editor": "^0.6.0", + "@hcengineering/text-editor-assets": "^0.6.0", + "@hcengineering/text-editor-resources": "^0.6.0", + "@hcengineering/board": "^0.6.19", + "@hcengineering/board-assets": "^0.6.19", + "@hcengineering/board-resources": "^0.6.0", + "@hcengineering/hr": "^0.6.19", + "@hcengineering/hr-assets": "^0.6.19", + "@hcengineering/hr-resources": "^0.6.0", + "@hcengineering/bitrix": "^0.6.52", + "@hcengineering/bitrix-assets": "^0.6.0", + "@hcengineering/bitrix-resources": "^0.6.0", + "@hcengineering/request": "^0.6.14", + "@hcengineering/request-assets": "^0.6.0", + "@hcengineering/request-resources": "^0.6.0", + "@hcengineering/drive": "^0.6.0", + "@hcengineering/drive-assets": "^0.6.0", + "@hcengineering/drive-resources": "^0.6.0", + "@hcengineering/support": "^0.6.5", + "@hcengineering/support-assets": "^0.6.5", + "@hcengineering/support-resources": "^0.6.0", + "@hcengineering/diffview": "^0.6.0", + "@hcengineering/diffview-assets": "^0.6.0", + "@hcengineering/diffview-resources": "^0.6.0", + "@hcengineering/time": "^0.6.0", + "@hcengineering/time-assets": "^0.6.0", + "@hcengineering/time-resources": "^0.6.0", + "@hcengineering/github": "^0.6.0", + "@hcengineering/github-assets": "^0.6.0", + "@hcengineering/github-resources": "^0.6.0", + "@hcengineering/desktop-preferences": "^0.6.0", + "@hcengineering/desktop-preferences-assets": "^0.6.0", + "@hcengineering/desktop-preferences-resources": "^0.6.0", + "@hcengineering/document": "^0.6.0", + "@hcengineering/document-assets": "^0.6.0", + "@hcengineering/document-resources": "^0.6.0", + "@hcengineering/love": "^0.6.0", + "@hcengineering/love-assets": "^0.6.0", + "@hcengineering/love-resources": "^0.6.0", + "@hcengineering/sign": "^0.6.0", + "@hcengineering/print": "^0.6.0", + "@hcengineering/print-assets": "^0.6.0", + "@hcengineering/print-resources": "^0.6.0", + "@hcengineering/guest": "^0.6.4", + "@hcengineering/guest-assets": "^0.6.0", + "@hcengineering/guest-resources": "^0.6.0", + "@hcengineering/uploader": "^0.6.0", + "@hcengineering/uploader-assets": "^0.6.0", + "@hcengineering/uploader-resources": "^0.6.0", + "@hcengineering/controlled-documents": "^0.1.0", + "@hcengineering/controlled-documents-assets": "^0.1.0", + "@hcengineering/controlled-documents-resources": "^0.1.0", + "@hcengineering/questions": "^0.1.0", + "@hcengineering/questions-assets": "^0.1.0", + "@hcengineering/questions-resources": "^0.1.0", + "@hcengineering/training": "^0.1.0", + "@hcengineering/training-assets": "^0.1.0", + "@hcengineering/training-resources": "^0.1.0", + "@hcengineering/server-training": "^0.1.0", + "@hcengineering/server-training-resources": "^0.1.0", + "@hcengineering/products": "^0.1.0", + "@hcengineering/products-assets": "^0.1.0", + "@hcengineering/products-resources": "^0.1.0", + "@hcengineering/analytics-collector": "^0.6.0", + "@hcengineering/analytics-collector-assets": "^0.6.0", + "electron-squirrel-startup": "~1.0.0", + "dotenv": "~16.0.0", + "electron-context-menu": "^4.0.1", + "electron-windows-badge": "^1.1.0", + "svelte": "^4.2.12", + "commander": "^8.1.0", + "electron-store": "^10.0.0", + "electron-log": "^5.1.7", + "electron-updater": "^6.2.1", + "livekit-client": "^2.0.10" + }, + "productName": "Huly Desktop", + "description": "Huly Desktop experience", + "keywords": [ + "electron", + "typescript", + "svelte" + ] +} diff --git a/desktop/postcss.config.js b/desktop/postcss.config.js new file mode 100644 index 0000000000..88752c6cb0 --- /dev/null +++ b/desktop/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + require('autoprefixer') + ] +} diff --git a/desktop/public/AppIcon.png b/desktop/public/AppIcon.png new file mode 100644 index 0000000000..e61b0bf112 Binary files /dev/null and b/desktop/public/AppIcon.png differ diff --git a/desktop/readme.md b/desktop/readme.md new file mode 100644 index 0000000000..796850e818 --- /dev/null +++ b/desktop/readme.md @@ -0,0 +1,12 @@ +## electron-builder + +The `package.json` has a `build` section for running +[electron-builder](https://www.electron.build/). I didn't add it +to this project's dependency list because many people prefer to install +it globally. If you do not wish to use electon-builder, simply +disregard or remove the `build` section from `package.json`. + +Note that this project's build configuration overrides the default +output directory to be `deploy` instead of `dist`, since `dist` is +already being used for the transpilation target. The `dist` directory +is the **source** for electron-builder. diff --git a/desktop/src/main/args.ts b/desktop/src/main/args.ts new file mode 100644 index 0000000000..b118841bac --- /dev/null +++ b/desktop/src/main/args.ts @@ -0,0 +1,33 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { OptionValues, program } from 'commander' + +program + .name('Huly') + .allowUnknownOption() + .option('-s, --server ', 'Remote server URL (front). E.g. https://huly.app') + +let opts: OptionValues | null = null + +export function getOptions (): OptionValues { + if (opts === null) { + program.parse(process.argv.slice(1), { from: 'user' }) + + opts = program.opts() + } + + return opts +} diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts new file mode 100644 index 0000000000..ca7dbddbb8 --- /dev/null +++ b/desktop/src/main/menu.ts @@ -0,0 +1,46 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Menu, MenuItemConstructorOptions } from 'electron' + +const isMac = process.platform === 'darwin' + +export const addMenus = (sendCommand: (cmd: string, ...args: any[]) => void): void => { + const template: MenuItemConstructorOptions[] = [ + { + label: 'File', + submenu: [ + { + label: 'Settings', + accelerator: 'Meta+,', + click: () => sendCommand('open-settings') + }, + { + label: 'Logout', + click: () => sendCommand('logout') + }, + { role: isMac ? 'close' : 'quit' } + ] + }, + { role: 'editMenu' }, + { role: 'viewMenu' }, + { role: 'windowMenu' } + ] + if (isMac) { + template.unshift({ role: 'appMenu' }) + } + const menu = Menu.buildFromTemplate(template) + Menu.setApplicationMenu(menu) +} diff --git a/desktop/src/main/permissions.ts b/desktop/src/main/permissions.ts new file mode 100644 index 0000000000..8c74b75ba2 --- /dev/null +++ b/desktop/src/main/permissions.ts @@ -0,0 +1,84 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Session, systemPreferences } from 'electron' +import log from 'electron-log' + +export function addPermissionHandlers (session: Session): void { + session.setPermissionRequestHandler((webContents, permission, result, details) => { + log.info('permissions requested: ', permission, details) + + if (process.platform !== 'darwin') { + result(true) + return + } + + if (permission !== 'media') { + result(true) + return + } + + const audioGranted = (details as any).mediaTypes?.includes('audio') === true ? askForMediaAccess('microphone') : Promise.resolve(true) + const videoGranted = (details as any).mediaTypes?.includes('video') === true ? askForMediaAccess('camera') : Promise.resolve(true) + + Promise.all([audioGranted, videoGranted]).then( + (res) => { result(res.every(r => r)) }, + () => { result(false) } + ) + }) + + session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => { + if (process.platform !== 'darwin') { + return true + } + + if (permission !== 'media') { + return true + } + + if (details.mediaType === 'audio') { + return systemPreferences.getMediaAccessStatus('microphone') === 'granted' + } + + if (details.mediaType === 'video') { + return systemPreferences.getMediaAccessStatus('camera') === 'granted' + } + + return false + }) +} + +async function askForMediaAccess (type: 'microphone' | 'camera'): Promise { + try { + if (process.platform !== 'darwin') { + return true + } + + const status = systemPreferences.getMediaAccessStatus(type) + log.info(`Current ${type} access status:`, status) + + if (status === 'not-determined') { + const success = await systemPreferences.askForMediaAccess(type) + log.info(`Result of ${type} access:`, success ? 'granted' : 'denied') + return success + } + + return status === 'granted' + } catch (error) { + log.error(`Could not get ${type} permission:`, error.message) + } + + return false +} diff --git a/desktop/src/main/start.ts b/desktop/src/main/start.ts new file mode 100644 index 0000000000..55d410dade --- /dev/null +++ b/desktop/src/main/start.ts @@ -0,0 +1,397 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { BrowserWindow, CookiesSetDetails, Notification, app, ipcMain, nativeImage, shell, dialog, systemPreferences, desktopCapturer } from 'electron' +import contextMenu from 'electron-context-menu' +import WinBadge from 'electron-windows-badge' +import Store from 'electron-store' +import * as path from 'path' +import { config as dotenvConfig } from 'dotenv' +import log from 'electron-log' +import { ProgressInfo, UpdateInfo } from 'electron-updater' + +import autoUpdater from './updater' +import { NotificationParams, Config } from '../ui/types' +import { addMenus } from './menu' +import { getOptions } from './args' +import { addPermissionHandlers } from './permissions' + +let mainWindow: BrowserWindow | undefined +let winBadge: any + +const isMac = process.platform === 'darwin' +const isWindows = process.platform === 'win32' +const isDev = process.env.NODE_ENV === 'development' + +const sessionPartition = !isDev ? 'persist:huly' : 'persist:huly_dev' +const iconKey = path.join(app.getAppPath(), 'dist', 'ui', 'public', 'AppIcon.png') + +const defaultWidth = 1440 +const defaultHeight = 960 + +const envPath = path.join(app.getAppPath(), isDev ? '.env-dev' : '.env') +console.log('do loading env from', envPath) +dotenvConfig({ + path: envPath +}) +const options = getOptions() + +// Note: using electron-store here as local storage is not available in the main process +// before the window is created +const settings = new Store() +const oldFront = readServerUrl() + +if (options.server !== undefined) { + ;(settings as any).set('server', options.server) +} + +const FRONT_URL = readServerUrl() +const serverChanged = oldFront !== FRONT_URL + +function readServerUrl (): string { + if (isDev) { + return process.env.FRONT_URL ?? 'http://localhost:8087' + } + + return ((settings as any).get('server', process.env.FRONT_URL) as string) ?? 'https://huly.app' +} + +// Handle creating/removing shortcuts on Windows when installing/uninstalling. +if (require('electron-squirrel-startup') === true) { + app.quit() +} + +console.log('Running Huly', process.env.MODEL_VERSION, isMac, isDev, process.env.NODE_ENV) + +function hookOpenWindow (window: BrowserWindow): void { + window.webContents.setWindowOpenHandler(({ url }) => { + console.log('opening window', url) + + /* + We need to detect if url is "our" or external. We should + open external urls in system browser + + When we open local URLs it would be as file:///workbench/my-spc/tracker/TSK-2 + As we load only our index.html there is no security problem to pass such URLs + to open arg as well + */ + if (url.indexOf(FRONT_URL) !== 0 && url.indexOf('file://') !== 0) { + void shell.openExternal(url) + } else { + void (async (): Promise => { + const bounds = mainWindow?.getBounds() + const childWindow = new BrowserWindow({ + width: bounds?.width ?? defaultWidth, + height: bounds?.height ?? defaultHeight, + x: (bounds?.x ?? 0) + 25, + y: (bounds?.y ?? 0) + 25, + titleBarStyle: isMac ? 'hidden' : 'default', + trafficLightPosition: { x: 10, y: 10 }, + icon: nativeImage.createFromPath(iconKey), + webPreferences: { + devTools: true, + sandbox: false, + partition: sessionPartition, + preload: path.join(app.getAppPath(), 'dist', 'main', 'preload.js'), + additionalArguments: [ + `--open=${encodeURI( + new URL(url).pathname + .split('/') + .filter((it) => it.length > 0) + .join('/') + )}` + ] + } + }) + // await childWindow.webContents.openDevTools() + await childWindow.loadFile(path.join('dist', 'ui', 'index.html')) + hookOpenWindow(childWindow) + })() + } + return { action: 'deny' } + }) +} + +const createWindow = async (): Promise => { + mainWindow = new BrowserWindow({ + width: defaultWidth, + height: defaultHeight, + titleBarStyle: isMac ? 'hidden' : 'default', + trafficLightPosition: { x: 10, y: 10 }, + icon: nativeImage.createFromPath(iconKey), + webPreferences: { + devTools: true, + sandbox: false, + backgroundThrottling: false, + partition: sessionPartition, + preload: path.join(app.getAppPath(), 'dist', 'main', 'preload.js') + } + }) + app.dock?.setIcon(nativeImage.createFromPath(iconKey)) + // await mainWindow.webContents.openDevTools() + if (isDev) { + mainWindow.webContents.openDevTools() + } + await mainWindow.loadFile(path.join('dist', 'ui', 'index.html')) + addPermissionHandlers(mainWindow.webContents.session) + + // In this example, only windows with the `about:blank` url will be created. + // All other urls will be blocked. + hookOpenWindow(mainWindow) + + if (isMac) { + mainWindow.on('close', (event) => { + // Prevent the default behavior (which would quit the app) + event.preventDefault() + // Hide the window + mainWindow?.hide() + }) + } else if (isWindows) { + winBadge = new WinBadge(mainWindow, { + font: '14px arial' + }) + } +} + +addMenus((cmd: string, ...args: any[]) => { + mainWindow?.webContents.send(cmd, args) +}) + +contextMenu({ + showSaveImageAs: false, + showInspectElement: false, + showSelectAll: false +}) + +ipcMain.on('set-badge', (event, badge: number) => { + app.dock?.setBadge(badge > 0 ? `${badge}` : '') + app.badgeCount = badge + + if (isWindows && winBadge !== undefined) { + winBadge.update(badge) + } +}) + +ipcMain.on('dock-bounce', (event) => { + app.dock?.bounce('informational') +}) + +ipcMain.on('send-notification', (event, notificationParams: NotificationParams) => { + if (Notification.isSupported()) { + const notification = new Notification(notificationParams) + + notification.on('click', () => { + mainWindow?.show() + mainWindow?.webContents.send('handle-notification-navigation') + }) + + notification.show() + } +}) + +ipcMain.on('set-title', (event, title) => { + const webContents = event.sender + const win = BrowserWindow.fromWebContents(webContents) + win?.setTitle(title) +}) + +ipcMain.on('set-combined-config', (event, config: Config) => { + log.info('Config set: ', config) + + const updatesUrl = process.env.DESKTOP_UPDATES_URL ?? config.DESKTOP_UPDATES_URL ?? 'https://dist.huly.io' + const updatesChannel = process.env.DESKTOP_UPDATES_CHANNEL ?? config.DESKTOP_UPDATES_CHANNEL ?? 'huly' + + autoUpdater.setFeedURL({ + provider: 'generic', + url: updatesUrl, + channel: updatesChannel + }) + void autoUpdater.checkForUpdatesAndNotify() +}) + +ipcMain.handle('get-main-config', (event, path) => { + const cfg = { + CONFIG_URL: process.env.CONFIG_URL ?? '', + FRONT_URL, + INITIAL_URL: process.env.INITIAL_URL ?? '', + MODEL_VERSION: process.env.MODEL_VERSION ?? '' + } + return cfg +}) +ipcMain.handle('get-host', (event, path) => { + return new URL(FRONT_URL).host +}) + +ipcMain.on('set-front-cookie', function (event, host: string, name: string, value: string) { + const webContents = event.sender + const win = BrowserWindow.fromWebContents(webContents) + const cv: CookiesSetDetails = { + url: host, + name, + value, + path: '/', + secure: true, + sameSite: 'no_restriction', + httpOnly: true, + domain: process.env.FRONT_DOMAIN + } + void win?.webContents?.session.cookies.set(cv) +}) + +const DEEP_LINKS_PROTOCOL = 'huly' +/* + Copy-paste from official tutorial for deep links + https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app +*/ +if (process.defaultApp) { + if (process.argv.length >= 2) { + app.setAsDefaultProtocolClient(DEEP_LINKS_PROTOCOL, process.execPath, [path.resolve(process.argv[1])]) + } +} else { + app.setAsDefaultProtocolClient(DEEP_LINKS_PROTOCOL) +} + +const gotTheLock = app.requestSingleInstanceLock() + +if (!gotTheLock) { + app.quit() +} + +let deepLink = '' +let haveDeepLinkHandler = false +function tryToOpenDeepLink (link?: string): void { + if (mainWindow == null || !haveDeepLinkHandler) { + if (link != null) { + deepLink = link + } + return + } + + const url = link ?? deepLink + const httpsUrl = url.replace(`${DEEP_LINKS_PROTOCOL}://`, 'https://') + mainWindow.webContents.send('handle-deep-link', httpsUrl) +} + +// Add windows / linux handler for second instance +// This is because Windows try to open second instance from the deep link +app.on('second-instance', (event, commandLine, workingDirectory) => { + if (mainWindow != null) { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.focus() + } + + const url = commandLine.pop() + console.log('Opening url', url) + + if (url != null) { + tryToOpenDeepLink(url) + } +}) + +ipcMain.on('on-deep-link-handler', () => { + haveDeepLinkHandler = true + tryToOpenDeepLink() +}) + +ipcMain.handle('get-screen-access', () => systemPreferences.getMediaAccessStatus('screen') === 'granted') +ipcMain.handle('get-screen-sources', () => { + return desktopCapturer.getSources({ types: ['window', 'screen'], fetchWindowIcons: true, thumbnailSize: { width: 225, height: 135 } }).then(async sources => { + return sources.map(source => { + return { + ...source, + appIconURL: source.appIcon?.toDataURL(), + thumbnailURL: source.thumbnail.toDataURL() + } + }) + }) +}) + +// MacOS implementation +// Handle the protocol. In this case, we choose to show an Error Box. +app.on('open-url', (event, url) => { + if (url == null) { + return + } + tryToOpenDeepLink(url) +}) + +async function onReady (): Promise { + await createWindow() + + const customProtocolArg = process.argv.find((arg) => arg.startsWith(`${DEEP_LINKS_PROTOCOL}://`)) ?? '' + tryToOpenDeepLink(customProtocolArg) + + if (serverChanged) { + mainWindow?.webContents.send('logout') + } +} + +app.on('ready', () => { + void onReady() +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +if (isMac) { + app.on('activate', () => { + if (mainWindow != null && (mainWindow.isMinimized() || !mainWindow.isVisible())) { + mainWindow.show() + } + }) + + app.on('before-quit', () => { + // Note: in case the app is exited by auto-updater all windows will be destroyed at this point + if (mainWindow === undefined || mainWindow.isDestroyed()) return + + mainWindow?.removeAllListeners('close') + mainWindow?.close() + }) +} + +autoUpdater.on('update-available', (info: UpdateInfo) => { + void dialog + .showMessageBox({ + type: 'info', + buttons: ['Update & Restart', 'Quit'], + defaultId: 0, + message: `A new version ${info.version} is available and it is required to continue. It will be downloaded and installed automatically.` + }) + .then(({ response }) => { + log.info(`Update dialog exit code: ${response}`) // eslint-disable-line no-console + + if (response !== 0) { + app.quit() + } + }) +}) + +autoUpdater.on('download-progress', (progressObj: ProgressInfo) => { + if (mainWindow === undefined) return + + mainWindow.setProgressBar(progressObj.percent / 100) + mainWindow.webContents.send('handle-update-download-progress', progressObj.percent) +}) + +autoUpdater.on('update-downloaded', (info) => { + // We have listeners that prevents the app from being exited on mac + app.removeAllListeners('window-all-closed') + mainWindow?.removeAllListeners('close') + + autoUpdater.quitAndInstall() +}) diff --git a/desktop/src/main/updater.ts b/desktop/src/main/updater.ts new file mode 100644 index 0000000000..dccae169b2 --- /dev/null +++ b/desktop/src/main/updater.ts @@ -0,0 +1,42 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import log from 'electron-log' +import { autoUpdater } from 'electron-updater' + +autoUpdater.logger = log + +autoUpdater.on('update-available', (info) => { + log.info('Update available. ', info) +}) +autoUpdater.on('update-not-available', (info) => { + log.info('Update not available.') +}) +autoUpdater.on('error', (err) => { + log.info('Error in auto-updater. ' + err) +}) +autoUpdater.on('download-progress', (progressObj) => { + let msg = 'Download speed: ' + progressObj.bytesPerSecond + + msg = msg + ' - Downloaded ' + progressObj.percent + '%' + msg = msg + ' (' + progressObj.transferred + '/' + progressObj.total + ')' + log.info(msg) +}) + +autoUpdater.on('update-downloaded', (info) => { + log.info('Update downloaded') +}) + +export default autoUpdater diff --git a/desktop/src/ui/index.ejs b/desktop/src/ui/index.ejs new file mode 100644 index 0000000000..57a6957751 --- /dev/null +++ b/desktop/src/ui/index.ejs @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/desktop/src/ui/index.ts b/desktop/src/ui/index.ts new file mode 100644 index 0000000000..5d583d2135 --- /dev/null +++ b/desktop/src/ui/index.ts @@ -0,0 +1,77 @@ +import login, { loginId } from '@hcengineering/login' +import { setMetadata, getMetadata } from '@hcengineering/platform' +import presentation, { closeClient, setDownloadProgress } from '@hcengineering/presentation' +import { settingId } from '@hcengineering/setting' +import { + closePanel, + closePopup, + createApp, + fetchMetadataLocalStorage, + getCurrentLocation, + getCurrentResolvedLocation, + navigate, + parseLocation, + setMetadataLocalStorage +} from '@hcengineering/ui' + +import { workbenchId } from '@hcengineering/workbench' +import { notificationId } from '@hcengineering/notification' + +import { configurePlatform } from './platform' +import { IPCMainExposed } from './types' +import { defineScreenShare } from './screenShare' + +defineScreenShare() + +void configurePlatform().then(() => { + createApp(document.body) +}) + +window.addEventListener('DOMContentLoaded', () => { + const ipcMain = (window as any).electron as IPCMainExposed + + ipcMain.on('open-settings', () => { + closePopup() + closePanel() + const loc = getCurrentResolvedLocation() + loc.fragment = undefined + loc.query = undefined + loc.path[2] = settingId + loc.path.length = 3 + navigate(loc) + }) + + ipcMain.on('logout', () => { + const tokens = fetchMetadataLocalStorage(login.metadata.LoginTokens) + if (tokens !== null) { + const loc = getCurrentLocation() + loc.path.splice(1, 1) + setMetadataLocalStorage(login.metadata.LoginTokens, tokens) + } + setMetadata(presentation.metadata.Token, null) + setMetadataLocalStorage(login.metadata.LastToken, null) + setMetadataLocalStorage(login.metadata.LoginEndpoint, null) + setMetadataLocalStorage(login.metadata.LoginEmail, null) + void closeClient().then(() => { + navigate({ path: [loginId] }) + }) + }) + + ipcMain.handleDeepLink((urlString) => { + const urlObject = new URL(urlString) + navigate(parseLocation(urlObject)) + }) + + ipcMain.handleNotificationNavigation(() => { + // For now navigate only to Inbox + const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin + const location = getCurrentResolvedLocation() + const urlString = `${frontUrl}/${workbenchId}/${location.path[1]}/${notificationId}` + const urlObject = new URL(urlString) + navigate(parseLocation(urlObject)) + }) + + ipcMain.handleUpdateDownloadProgress((progress) => { + setDownloadProgress(progress) + }) +}) diff --git a/desktop/src/ui/notifications.ts b/desktop/src/ui/notifications.ts new file mode 100644 index 0000000000..7b37aa8e82 --- /dev/null +++ b/desktop/src/ui/notifications.ts @@ -0,0 +1,205 @@ +import contact, { PersonAccount, formatName } from '@hcengineering/contact' +import { Ref, TxOperations } from '@hcengineering/core' +import notification, { DocNotifyContext, CommonInboxNotification, ActivityInboxNotification, InboxNotification } from '@hcengineering/notification' +import { IntlString, addEventListener, translate } from '@hcengineering/platform' +import { getClient } from '@hcengineering/presentation' +import { location } from '@hcengineering/ui' +import workbench, { workbenchId } from '@hcengineering/workbench' +import desktopPreferences, { defaultNotificationPreference } from '@hcengineering/desktop-preferences' +import { activePreferences } from '@hcengineering/desktop-preferences-resources' +import { InboxNotificationsClientImpl, getDisplayInboxData } from '@hcengineering/notification-resources' + +import { IPCMainExposed } from './types' + +let client: TxOperations + +async function hydrateNotificationAsYouCan (lastNotification: InboxNotification): Promise<{ title: string, body: string } | undefined> { + // Let's try to do our best and figure out from who we have an notification + + if (client === undefined) { + return undefined + } + + if (lastNotification === undefined) { + return undefined + } + + let intlTitle: IntlString | undefined + let titleParams + + if (lastNotification._class === notification.class.CommonInboxNotification) { + intlTitle = (lastNotification as CommonInboxNotification).message + titleParams = (lastNotification as CommonInboxNotification).props + } else if (lastNotification._class === notification.class.ActivityInboxNotification) { + intlTitle = (lastNotification as ActivityInboxNotification).title + titleParams = (lastNotification as ActivityInboxNotification).intlParams + } + + if (intlTitle !== undefined && lastNotification.body !== undefined) { + const intlParams = lastNotification.intlParams ?? {} + + if (lastNotification.intlParamsNotLocalized !== undefined) { + for (const param in lastNotification.intlParamsNotLocalized) { + const value = lastNotification.intlParamsNotLocalized[param] + intlParams[param] = await translate(value, {}) + } + } + const title = await translate(intlTitle, titleParams ?? {}) + const body = await translate(lastNotification.body, lastNotification.intlParams ?? {}) + + // Do not show notification if there is no translate + if (title === intlTitle as string || body === lastNotification.body as string) { + return undefined + } + + return { title, body } + } + + const title = await translate(desktopPreferences.string.HaveGotANotification, {}) + + // Do not show notification if there is no translate + if (title === lastNotification.title as string) { + return undefined + } + + const noPersonData = { + title, + body: '' + } + + const account = await client.findOne(contact.class.PersonAccount, { _id: lastNotification.modifiedBy as Ref }) + + if (account == null) { + return noPersonData + } + + const person = await client.findOne(contact.class.Person, { _id: account.person }) + + if (person == null) { + return noPersonData + } + + return { + title, + body: formatName(person.name) + } +} + +function getLasUnViewedNotification ( + notifications: InboxNotification[], + notificationHistory: Map +): InboxNotification | undefined { + let lastNotification + let lastTime = 0 + + for (const n of notifications) { + if (notificationHistory.has(n._id as string)) { + continue + } + + const createdOn = n.createdOn ?? n.modifiedOn + + notificationHistory.set(n._id as string, createdOn) + + if (createdOn > lastTime) { + lastTime = createdOn + lastNotification = n + } + } + + return lastNotification +} + +/** + * @public + */ +export function configureNotifications (): void { + let preferences = defaultNotificationPreference + let prevUnViewdNotificationsCount = 0 + + // For now we want to track all notifications which happends after the launch + // because we generate them on a client + let initTimestamp = 0 + const notificationHistory = new Map() + + addEventListener(workbench.event.NotifyConnection, async (event, account: PersonAccount) => { + client = getClient() + const electronAPI: IPCMainExposed = (window as any).electron + + const inboxClient = InboxNotificationsClientImpl.getClient() + + async function handleNotifications (notificationsByContext: Map, InboxNotification[]>): Promise { + const inboxData = await getDisplayInboxData(notificationsByContext) + + if (notificationHistory.size === 0) { + for (const [, notifications] of inboxData) { + for (const n of notifications) { + notificationHistory.set(n._id as string, n.createdOn ?? n.modifiedOn) + } + } + } + + const unViewedNotifications: InboxNotification[] = Array.from(inboxData.values()).flat().filter(({ isViewed }) => !isViewed) + // const notificationsAfterLaunch = notifications.filter((p) => p.txes.some((p) => p.modifiedOn > initTimestamp)) + // We need to get the most recent notifications + + if (prevUnViewdNotificationsCount !== unViewedNotifications.length) { + if (preferences.showUnreadCounter) { + electronAPI.setBadge(unViewedNotifications.length) + } + if (preferences.bounceAppIcon) { + electronAPI.dockBounce() + } + prevUnViewdNotificationsCount = unViewedNotifications.length + } + + const notification = getLasUnViewedNotification(unViewedNotifications, notificationHistory) + + if (preferences.showNotifications && initTimestamp > 0 && notification !== undefined) { + // const notification = notificationsAfterLaunch[notificationsAfterLaunch.length - 1] + const notificationData = await hydrateNotificationAsYouCan(notification) + if (notificationData !== undefined) { + if (notificationData.body === '') { + notificationData.body = await translate(desktopPreferences.string.TotalNotificationsCount, { + count: prevUnViewdNotificationsCount + }) + } + + electronAPI.sendNotification({ + silent: !preferences.playSound, + ...notificationData + }) + } + } + + if (initTimestamp === 0) { + initTimestamp = Date.now() + } + } + + inboxClient.inboxNotificationsByContext.subscribe(data => { + void handleNotifications(data) + }) + + activePreferences.subscribe((newPreferences) => { + if (preferences.showUnreadCounter && !newPreferences.showUnreadCounter) { + electronAPI.setBadge(0) + } + if (!preferences.showUnreadCounter && newPreferences.showUnreadCounter) { + electronAPI.setBadge(prevUnViewdNotificationsCount) + } + preferences = newPreferences + }) + }) + + addEventListener(workbench.event.NotifyTitle, async (event, title: string) => { + ;((window as any).electron as IPCMainExposed).setTitle(title) + }) + + location.subscribe((location) => { + if (!(location.path[0] === workbenchId || location.path[0] === workbench.component.WorkbenchApp)) { + // We need to clear badge + ;((window as any).electron as IPCMainExposed).setBadge(0) + } + }) +} diff --git a/desktop/src/ui/platform.ts b/desktop/src/ui/platform.ts new file mode 100644 index 0000000000..6b3085f3d9 --- /dev/null +++ b/desktop/src/ui/platform.ts @@ -0,0 +1,340 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +import { + Plugin, + addEventListener, + addLocation, + addStringsLoader, + getMetadata, + platformId, + setMetadata +} from '@hcengineering/platform' + +import { activityId } from '@hcengineering/activity' +import { attachmentId } from '@hcengineering/attachment' +import { boardId } from '@hcengineering/board' +import calendar, { calendarId } from '@hcengineering/calendar' +import { chunterId } from '@hcengineering/chunter' +import client, { clientId } from '@hcengineering/client' +import contactPlugin, { contactId } from '@hcengineering/contact' +import { documentId } from '@hcengineering/document' +import { driveId } from '@hcengineering/drive' +import gmail, { gmailId } from '@hcengineering/gmail' +import { hrId } from '@hcengineering/hr' +import { imageCropperId } from '@hcengineering/image-cropper' +import { inventoryId } from '@hcengineering/inventory' +import { leadId } from '@hcengineering/lead' +import login, { loginId } from '@hcengineering/login' +import notification, { notificationId } from '@hcengineering/notification' +import { recruitId } from '@hcengineering/recruit' +import rekoni from '@hcengineering/rekoni' +import { requestId } from '@hcengineering/request' +import { settingId } from '@hcengineering/setting' +import { supportId } from '@hcengineering/support' +import { tagsId } from '@hcengineering/tags' +import { taskId } from '@hcengineering/task' +import telegram, { telegramId } from '@hcengineering/telegram' +import { templatesId } from '@hcengineering/templates' +import tracker, { trackerId } from '@hcengineering/tracker' +import uiPlugin, { getCurrentLocation, locationStorageKeyId, navigate, setLocationStorageKey } from '@hcengineering/ui' +import { uploaderId } from '@hcengineering/uploader' +import { viewId } from '@hcengineering/view' +import workbench, { workbenchId } from '@hcengineering/workbench' +import { diffviewId } from '@hcengineering/diffview' +import { timeId } from '@hcengineering/time' +import { desktopPreferencesId } from '@hcengineering/desktop-preferences' +import guest, { guestId } from '@hcengineering/guest' +import { bitrixId } from '@hcengineering/bitrix' +import { productsId } from '@hcengineering/products' +import { questionsId } from '@hcengineering/questions' +import { trainingId } from '@hcengineering/training' +import { documentsId } from '@hcengineering/controlled-documents' + +import '@hcengineering/activity-assets' +import '@hcengineering/attachment-assets' +import '@hcengineering/bitrix-assets' +import '@hcengineering/board-assets' +import '@hcengineering/calendar-assets' +import '@hcengineering/chunter-assets' +import '@hcengineering/contact-assets' +import '@hcengineering/document-assets' +import '@hcengineering/drive-assets' +import '@hcengineering/gmail-assets' +import '@hcengineering/hr-assets' +import '@hcengineering/inventory-assets' +import '@hcengineering/lead-assets' +import '@hcengineering/login-assets' +import '@hcengineering/notification-assets' +import '@hcengineering/preference-assets' +import '@hcengineering/recruit-assets' +import '@hcengineering/request-assets' +import '@hcengineering/setting-assets' +import '@hcengineering/support-assets' +import '@hcengineering/tags-assets' +import '@hcengineering/task-assets' +import '@hcengineering/telegram-assets' +import '@hcengineering/templates-assets' +import '@hcengineering/tracker-assets' +import '@hcengineering/view-assets' +import '@hcengineering/workbench-assets' +import '@hcengineering/guest-assets' +import '@hcengineering/uploader-assets' +import '@hcengineering/diffview-assets' +import '@hcengineering/time-assets' +import '@hcengineering/desktop-preferences-assets' +import '@hcengineering/love-assets' +import '@hcengineering/print-assets' +import '@hcengineering/questions-assets' +import '@hcengineering/training-assets' +import '@hcengineering/products-assets' +import '@hcengineering/controlled-documents-assets' +import '@hcengineering/analytics-collector-assets' + +import { coreId } from '@hcengineering/core' +import presentation, { parsePreviewConfig, presentationId } from '@hcengineering/presentation' +import textEditor, { textEditorId } from '@hcengineering/text-editor' +import love, { loveId } from '@hcengineering/love' +import print, { printId } from '@hcengineering/print' +import sign from '@hcengineering/sign' +import analyticsCollector, { analyticsCollectorId } from '@hcengineering/analytics-collector' + +import { setDefaultLanguage } from '@hcengineering/theme' +import { configureNotifications } from './notifications' +import { Config, IPCMainExposed, Branding } from './types' + +import '@hcengineering/github-assets' +import github, { githubId } from '@hcengineering/github' +import { uiId } from '@hcengineering/ui/src/plugin' +import { preferenceId } from '@hcengineering/preference' + +function configureI18n (): void { + // Add localization + addStringsLoader(platformId, async (lang: string) => await import(`@hcengineering/platform/lang/${lang}.json`)) + addStringsLoader(coreId, async (lang: string) => await import(`@hcengineering/core/lang/${lang}.json`)) + addStringsLoader( + presentationId, + async (lang: string) => await import(`@hcengineering/presentation/lang/${lang}.json`) + ) + addStringsLoader(textEditorId, async (lang: string) => await import(`@hcengineering/text-editor-assets/lang/${lang}.json`)) + addStringsLoader(uiId, async (lang: string) => await import(`@hcengineering/ui/lang/${lang}.json`)) + addStringsLoader(uploaderId, async (lang: string) => await import(`@hcengineering/uploader-assets/lang/${lang}.json`)) + addStringsLoader(activityId, async (lang: string) => await import(`@hcengineering/activity-assets/lang/${lang}.json`)) + addStringsLoader( + attachmentId, + async (lang: string) => await import(`@hcengineering/attachment-assets/lang/${lang}.json`) + ) + addStringsLoader(bitrixId, async (lang: string) => await import(`@hcengineering/bitrix-assets/lang/${lang}.json`)) + addStringsLoader(boardId, async (lang: string) => await import(`@hcengineering/board-assets/lang/${lang}.json`)) + addStringsLoader(calendarId, async (lang: string) => await import(`@hcengineering/calendar-assets/lang/${lang}.json`)) + addStringsLoader(chunterId, async (lang: string) => await import(`@hcengineering/chunter-assets/lang/${lang}.json`)) + addStringsLoader(contactId, async (lang: string) => await import(`@hcengineering/contact-assets/lang/${lang}.json`)) + addStringsLoader(driveId, async (lang: string) => await import(`@hcengineering/drive-assets/lang/${lang}.json`)) + addStringsLoader(gmailId, async (lang: string) => await import(`@hcengineering/gmail-assets/lang/${lang}.json`)) + addStringsLoader(hrId, async (lang: string) => await import(`@hcengineering/hr-assets/lang/${lang}.json`)) + addStringsLoader( + inventoryId, + async (lang: string) => await import(`@hcengineering/inventory-assets/lang/${lang}.json`) + ) + addStringsLoader(leadId, async (lang: string) => await import(`@hcengineering/lead-assets/lang/${lang}.json`)) + addStringsLoader(loginId, async (lang: string) => await import(`@hcengineering/login-assets/lang/${lang}.json`)) + addStringsLoader( + notificationId, + async (lang: string) => await import(`@hcengineering/notification-assets/lang/${lang}.json`) + ) + addStringsLoader( + preferenceId, + async (lang: string) => await import(`@hcengineering/preference-assets/lang/${lang}.json`) + ) + addStringsLoader(recruitId, async (lang: string) => await import(`@hcengineering/recruit-assets/lang/${lang}.json`)) + addStringsLoader(requestId, async (lang: string) => await import(`@hcengineering/request-assets/lang/${lang}.json`)) + addStringsLoader(settingId, async (lang: string) => await import(`@hcengineering/setting-assets/lang/${lang}.json`)) + addStringsLoader(supportId, async (lang: string) => await import(`@hcengineering/support-assets/lang/${lang}.json`)) + addStringsLoader(tagsId, async (lang: string) => await import(`@hcengineering/tags-assets/lang/${lang}.json`)) + addStringsLoader(taskId, async (lang: string) => await import(`@hcengineering/task-assets/lang/${lang}.json`)) + addStringsLoader(telegramId, async (lang: string) => await import(`@hcengineering/telegram-assets/lang/${lang}.json`)) + addStringsLoader( + templatesId, + async (lang: string) => await import(`@hcengineering/templates-assets/lang/${lang}.json`) + ) + addStringsLoader(trackerId, async (lang: string) => await import(`@hcengineering/tracker-assets/lang/${lang}.json`)) + addStringsLoader(viewId, async (lang: string) => await import(`@hcengineering/view-assets/lang/${lang}.json`)) + addStringsLoader( + workbenchId, + async (lang: string) => await import(`@hcengineering/workbench-assets/lang/${lang}.json`) + ) + + addStringsLoader( + desktopPreferencesId, + async (lang: string) => await import(`@hcengineering/desktop-preferences-assets/lang/${lang}.json`) + ) + addStringsLoader(diffviewId, async (lang: string) => await import(`@hcengineering/diffview-assets/lang/${lang}.json`)) + addStringsLoader(documentId, async (lang: string) => await import(`@hcengineering/document-assets/lang/${lang}.json`)) + addStringsLoader(timeId, async (lang: string) => await import(`@hcengineering/time-assets/lang/${lang}.json`)) + addStringsLoader(githubId, async (lang: string) => await import(`@hcengineering/github-assets/lang/${lang}.json`)) + addStringsLoader(documentsId, async (lang: string) => await import(`@hcengineering/controlled-documents-assets/lang/${lang}.json`)) + addStringsLoader(productsId, async (lang: string) => await import(`@hcengineering/products-assets/lang/${lang}.json`)) + addStringsLoader(questionsId, async (lang: string) => await import(`@hcengineering/questions-assets/lang/${lang}.json`)) + addStringsLoader(trainingId, async (lang: string) => await import(`@hcengineering/training-assets/lang/${lang}.json`)) + addStringsLoader(guestId, async (lang: string) => await import(`@hcengineering/guest-assets/lang/${lang}.json`)) + addStringsLoader(loveId, async (lang: string) => await import(`@hcengineering/love-assets/lang/${lang}.json`)) + addStringsLoader(printId, async (lang: string) => await import(`@hcengineering/print-assets/lang/${lang}.json`)) + addStringsLoader(analyticsCollectorId, async (lang: string) => await import(`@hcengineering/analytics-collector-assets/lang/${lang}.json`)) +} + +export async function configurePlatform (): Promise { + configureI18n() + + const ipcMain = (window as any).electron as IPCMainExposed + const config: Config = await ipcMain.config() + const myBranding: Branding = await ipcMain.branding() + // await (await fetch(devConfig? '/config-dev.json' : '/config.json')).json() + console.log('loading configuration', config) + console.log('loaded branding', myBranding) + + const title = myBranding.title ?? 'Huly Desktop' + ipcMain.setTitle(title) + + setMetadata(login.metadata.AccountsUrl, config.ACCOUNTS_URL) + setMetadata(presentation.metadata.UploadURL, config.UPLOAD_URL) + setMetadata(presentation.metadata.CollaboratorUrl, config.COLLABORATOR_URL) + setMetadata(presentation.metadata.CollaboratorApiUrl, config.COLLABORATOR_API_URL) + setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG)) + setMetadata(presentation.metadata.FrontUrl, config.FRONT_URL) + + setMetadata(textEditor.metadata.CollaboratorUrl, config.COLLABORATOR_URL) + + setMetadata(github.metadata.GithubApplication, config.GITHUB_APP ?? '') + setMetadata(github.metadata.GithubClientID, config.GITHUB_CLIENTID ?? '') + setMetadata(github.metadata.GithubURL, config.GITHUB_URL ?? '') + + if (config.MODEL_VERSION != null) { + console.log('Minimal Model version requirement', config.MODEL_VERSION) + setMetadata(presentation.metadata.RequiredVersion, config.MODEL_VERSION) + } + setMetadata(telegram.metadata.TelegramURL, config.TELEGRAM_URL ?? 'http://localhost:8086') + setMetadata(gmail.metadata.GmailURL, config.GMAIL_URL ?? 'http://localhost:8087') + setMetadata(calendar.metadata.CalendarServiceURL, config.CALENDAR_URL ?? 'http://localhost:8095') + setMetadata(notification.metadata.PushPublicKey, config.PUSH_PUBLIC_KEY) + + setMetadata(rekoni.metadata.RekoniUrl, config.REKONI_URL) + setMetadata(contactPlugin.metadata.LastNameFirst, myBranding.lastNameFirst === 'true' ?? false) + setMetadata(love.metadata.ServiceEnpdoint, config.LOVE_ENDPOINT) + setMetadata(love.metadata.WebSocketURL, config.LIVEKIT_WS) + setMetadata(print.metadata.PrintURL, config.PRINT_URL) + setMetadata(sign.metadata.SignURL, config.SIGN_URL) + setMetadata(uiPlugin.metadata.DefaultApplication, login.component.LoginApp) + setMetadata(analyticsCollector.metadata.EndpointURL, config.ANALYTICS_COLLECTOR_URL) + + const languages = myBranding.languages !== undefined && myBranding.languages !== '' ? myBranding.languages.split(',').map((l) => l.trim()) : ['en', 'ru', 'es', 'pt'] + + setMetadata(uiPlugin.metadata.Languages, languages) + + setMetadata( + uiPlugin.metadata.Routes, + new Map([ + [workbenchId, workbench.component.WorkbenchApp], + [loginId, login.component.LoginApp], + [calendarId, calendar.component.ConnectApp], + [guestId, guest.component.GuestApp] + ]) + ) + + addLocation(coreId, async () => ({ default: async () => ({}) })) + addLocation(presentationId, async () => ({ default: async () => ({}) })) + + addLocation(clientId, async () => await import('@hcengineering/client-resources')) + addLocation(loginId, async () => await import('@hcengineering/login-resources')) + addLocation(workbenchId, async () => await import('@hcengineering/workbench-resources')) + addLocation(viewId, async () => await import('@hcengineering/view-resources')) + addLocation(taskId, async () => await import('@hcengineering/task-resources')) + addLocation(contactId, async () => await import('@hcengineering/contact-resources')) + addLocation(chunterId, async () => await import('@hcengineering/chunter-resources')) + addLocation(recruitId, async () => await import('@hcengineering/recruit-resources')) + addLocation(activityId, async () => await import('@hcengineering/activity-resources')) + addLocation(settingId, async () => await import('@hcengineering/setting-resources')) + addLocation(leadId, async () => await import('@hcengineering/lead-resources')) + addLocation(telegramId, async () => await import('@hcengineering/telegram-resources')) + addLocation(attachmentId, async () => await import('@hcengineering/attachment-resources')) + addLocation(gmailId, async () => await import('@hcengineering/gmail-resources')) + addLocation(imageCropperId, async () => await import('@hcengineering/image-cropper-resources')) + addLocation(inventoryId, async () => await import('@hcengineering/inventory-resources')) + addLocation(templatesId, async () => await import('@hcengineering/templates-resources')) + addLocation(notificationId, async () => await import('@hcengineering/notification-resources')) + addLocation(tagsId, async () => await import('@hcengineering/tags-resources')) + addLocation(calendarId, async () => await import('@hcengineering/calendar-resources')) + + addLocation(trackerId, async () => await import('@hcengineering/tracker-resources')) + addLocation(boardId, async () => await import('@hcengineering/board-resources')) + addLocation(hrId, async () => await import('@hcengineering/hr-resources')) + addLocation(bitrixId, async () => await import('@hcengineering/bitrix-resources')) + addLocation(requestId, async () => await import('@hcengineering/request-resources')) + addLocation(driveId, async () => await import('@hcengineering/drive-resources')) + addLocation(supportId, async () => await import('@hcengineering/support-resources')) + addLocation(diffviewId, async () => await import('@hcengineering/diffview-resources')) + addLocation(documentId, async () => await import('@hcengineering/document-resources')) + addLocation(timeId, async () => await import('@hcengineering/time-resources')) + addLocation(questionsId, async () => await import('@hcengineering/questions-resources')) + addLocation(trainingId, async () => await import('@hcengineering/training-resources')) + addLocation(productsId, async () => await import('@hcengineering/products-resources')) + addLocation(documentsId, async () => await import('@hcengineering/controlled-documents-resources')) + addLocation(uploaderId, async () => await import('@hcengineering/uploader-resources')) + addLocation(githubId, async () => await import(/* webpackChunkName: "github" */ '@hcengineering/github-resources')) + addLocation( + desktopPreferencesId, + async () => await import(/* webpackChunkName: "desktop-preferences" */ '@hcengineering/desktop-preferences-resources') + ) + addLocation(guestId, () => import(/* webpackChunkName: "guest" */ '@hcengineering/guest-resources')) + addLocation(loveId, () => import(/* webpackChunkName: "love" */ '@hcengineering/love-resources')) + addLocation(printId, () => import(/* webpackChunkName: "print" */ '@hcengineering/print-resources')) + addLocation(textEditorId, () => import(/* webpackChunkName: "text-editor" */ '@hcengineering/text-editor-resources')) + + setMetadata(client.metadata.FilterModel, true) + setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin]) + + // Use binary response transfer for faster performance and small transfer sizes. + setMetadata(client.metadata.UseBinaryProtocol, true) + // Disable for now, since it causes performance issues on linux/docker/kubernetes boxes for now. + setMetadata(client.metadata.UseProtocolCompression, true) + + setMetadata(uiPlugin.metadata.PlatformTitle, title) + setMetadata(workbench.metadata.PlatformTitle, title) + setDefaultLanguage(myBranding.defaultLanguage ?? 'en') + setMetadata(workbench.metadata.DefaultApplication, myBranding.defaultApplication ?? 'tracker') + setMetadata(workbench.metadata.DefaultSpace, myBranding.defaultSpace ?? tracker.project.DefaultProject) + setMetadata(workbench.metadata.DefaultSpecial, myBranding.defaultSpecial ?? 'issues') + + addEventListener(workbench.event.NotifyConnection, async (evt) => { + await ipcMain.setFrontCookie( + config.FRONT_URL, + presentation.metadata.Token.replaceAll(':', '-'), + getMetadata(presentation.metadata.Token) ?? '' + ) + }) + + configureNotifications() + + if (config.INITIAL_URL !== '') { + setLocationStorageKey('uberflow_child') + } + + const last = localStorage.getItem(locationStorageKeyId) + if (config.INITIAL_URL !== '') { + console.log('NAVIGATE', config.INITIAL_URL, getCurrentLocation()) + // NavigationExpandedDefault=false fills buggy: + // — Navigator closes in unpredictable way + // — Many sections of the have have no default central content so without + // navigator is looks like something is broken + // Should consifer if we want to fix this + // setMetadata(workbench.metadata.NavigationExpandedDefault, false) + navigate({ + path: config.INITIAL_URL.split('/') + }) + } else if (last !== null) { + navigate(JSON.parse(last)) + } else { + navigate({ path: [] }) + } + console.log('Initial location is: ', getCurrentLocation()) +} diff --git a/desktop/src/ui/preload.ts b/desktop/src/ui/preload.ts new file mode 100644 index 0000000000..357cf71dec --- /dev/null +++ b/desktop/src/ui/preload.ts @@ -0,0 +1,113 @@ +// preload.js + +import { BrandingMap, Config, IPCMainExposed, NotificationParams } from './types' + +import { contextBridge, ipcRenderer } from 'electron' + +/** + * @public + */ +export function concatLink (host: string, path: string): string { + if (!host.endsWith('/') && !path.startsWith('/')) { + return `${host}/${path}` + } else if (host.endsWith('/') && path.startsWith('/')) { + const newPath = path.slice(1) + return `${host}${newPath}` + } else { + return `${host}${path}` + } +} + +const openArg = (process.argv.find((it) => it.startsWith('--open=')) ?? '').split('--open=')[1] +console.log('Open passed', openArg) +let configPromise: Promise | undefined + +const expose: IPCMainExposed = { + setBadge: (badge: number) => { + ipcRenderer.send('set-badge', badge) + }, + setTitle: (title: string) => { + ipcRenderer.send('set-title', title) + }, + dockBounce: () => { + ipcRenderer.send('dock-bounce') + }, + sendNotification: (notificationParams: NotificationParams) => { + ipcRenderer.send('send-notification', notificationParams) + }, + + config: async () => { + if (configPromise === undefined) { + configPromise = new Promise((resolve, reject) => { + ipcRenderer.invoke('get-main-config').then( + async (mainConfig) => { + const serverConfig = await (await fetch(concatLink(mainConfig.FRONT_URL, mainConfig.CONFIG_URL))).json() + const combinedConfig = { + ...serverConfig, + ...mainConfig, + INITIAL_URL: openArg ?? '', + UPLOAD_URL: concatLink(mainConfig.FRONT_URL, serverConfig.UPLOAD_URL), + MODEL_VERSION: mainConfig.MODEL_VERSION + } + + ipcRenderer.send('set-combined-config', combinedConfig) + + resolve(combinedConfig) + }, + (err) => { + reject(err) + } + ) + }) + } + + return await configPromise + }, + branding: async () => { + const cfg = await expose.config() + const branding: BrandingMap = await ( + await fetch(cfg.BRANDING_URL ?? concatLink(cfg.FRONT_URL, 'branding.json')) + ).json() + const host = await ipcRenderer.invoke('get-host') + return branding[host] ?? {} + }, + on: (event: string, op: (...args: any[]) => void) => { + ipcRenderer.on(event, (channel: any, args: any[]) => { + console.log('on handle', args) + op(channel, args) + }) + }, + + handleDeepLink: (callback) => { + ipcRenderer.on('handle-deep-link', (event, value) => { + try { + if (typeof value === 'string' && value !== '') { + callback(value) + } + } catch (e) { + // Just do nothing. Nothing is ok if there is something with URL + } + }) + ipcRenderer.send('on-deep-link-handler') + }, + + handleNotificationNavigation: (callback) => { + ipcRenderer.on('handle-notification-navigation', (event) => { + callback() + }) + }, + + handleUpdateDownloadProgress: (callback) => { + ipcRenderer.on('handle-update-download-progress', (event, value) => { + callback(value) + }) + }, + + async setFrontCookie (host: string, name: string, value: string): Promise { + ipcRenderer.send('set-front-cookie', host, name, value) + }, + + getScreenAccess: () => ipcRenderer.invoke('get-screen-access'), + getScreenSources: () => ipcRenderer.invoke('get-screen-sources') +} +contextBridge.exposeInMainWorld('electron', expose) diff --git a/desktop/src/ui/screenShare.ts b/desktop/src/ui/screenShare.ts new file mode 100644 index 0000000000..c7f121b430 --- /dev/null +++ b/desktop/src/ui/screenShare.ts @@ -0,0 +1,126 @@ +import log from 'electron-log' +import love from '@hcengineering/love' +import { setCustomCreateScreenTracks } from '@hcengineering/love-resources' +import { showPopup } from '@hcengineering/ui' +import { Track, LocalTrack, LocalAudioTrack, LocalVideoTrack, ParticipantEvent, TrackInvalidError, ScreenShareCaptureOptions, DeviceUnsupportedError, ScreenSharePresets } from 'livekit-client' + +import { IPCMainExposed } from './types' + +export function defineScreenShare (): void { + setCustomCreateScreenTracks(async function electronCreateScreenTracks (options?: ScreenShareCaptureOptions) { + const ipcMain = (window as any).electron as IPCMainExposed + const sources = await ipcMain.getScreenSources() + + const hasAccess = await ipcMain.getScreenAccess() + if (!hasAccess) { + log.error('No screen access granted') + throw new Error('No screen access granted') + } + + if (navigator.mediaDevices.getDisplayMedia === undefined) { + throw new DeviceUnsupportedError('getDisplayMedia not supported') + } + + return await new Promise((resolve, reject) => { + let wasSelected = false + + showPopup( + love.component.SelectScreenSourcePopup, + { + sources + }, + 'top', + () => { + if (!wasSelected) { + reject(new Error('No source selected')) + } + }, + (val) => { + if (val != null) { + wasSelected = true + if (options === undefined) { + options = {} + } + + if (options.resolution === undefined) { + options.resolution = ScreenSharePresets.h1080fps30.resolution + } + + const constraints = screenCaptureToDisplayMediaStreamOptions(options) + + if (constraints.video === undefined) { + log.error('Wrong video options specified') + throw new Error('Wrong video options specified') + } + + constraints.video = { + mandatory: { + ...(typeof constraints.video === 'boolean' ? {} : constraints.video), + chromeMediaSource: 'desktop', + chromeMediaSourceId: val + } + } as any + + void window.navigator.mediaDevices.getUserMedia(constraints).then((stream) => { + const tracks = stream.getVideoTracks() + if (tracks.length === 0) { + log.error('No video track found') + throw new TrackInvalidError('No video track found') + } + const screenVideo = new LocalVideoTrack(tracks[0], undefined, false, { + loggerName: this.roomOptions.loggerName, + loggerContextCb: () => this.logContext + }) + screenVideo.source = Track.Source.ScreenShare + if (options?.contentHint !== undefined) { + screenVideo.mediaStreamTrack.contentHint = options.contentHint + } + + const localTracks: Array = [screenVideo] + if (stream.getAudioTracks().length > 0) { + this.emit(ParticipantEvent.AudioStreamAcquired) + const screenAudio = new LocalAudioTrack( + stream.getAudioTracks()[0], + undefined, + false, + this.audioContext, + { loggerName: this.roomOptions.loggerName, loggerContextCb: () => this.logContext } + ) + screenAudio.source = Track.Source.ScreenShareAudio + localTracks.push(screenAudio) + } + resolve(localTracks) + }) + } + } + ) + }) + }) +} + +function screenCaptureToDisplayMediaStreamOptions ( + options: ScreenShareCaptureOptions +): DisplayMediaStreamOptions { + let videoConstraints: MediaTrackConstraints | boolean = options.video ?? true + // treat 0 as uncapped + if (options.resolution !== undefined && options.resolution.width > 0 && options.resolution.height > 0) { + videoConstraints = typeof videoConstraints === 'boolean' ? {} : videoConstraints + videoConstraints = { + ...videoConstraints, + width: { ideal: options.resolution.width }, + height: { ideal: options.resolution.height }, + frameRate: options.resolution.frameRate + } + } + + return { + audio: options.audio ?? false, + video: videoConstraints, + // @ts-expect-error support for experimental display media features + controller: options.controller, + selfBrowserSurface: options.selfBrowserSurface, + surfaceSwitching: options.surfaceSwitching, + systemAudio: options.systemAudio, + preferCurrentTab: options.preferCurrentTab + } +} diff --git a/desktop/src/ui/types.ts b/desktop/src/ui/types.ts new file mode 100644 index 0000000000..d87eec0466 --- /dev/null +++ b/desktop/src/ui/types.ts @@ -0,0 +1,79 @@ +import { ScreenSource } from '@hcengineering/love' + +/** + * @public + */ +export interface Config { + ACCOUNTS_URL: string + COLLABORATOR_URL: string + COLLABORATOR_API_URL: string + FRONT_URL: string + UPLOAD_URL: string + MODEL_VERSION?: string + TELEGRAM_URL: string + GMAIL_URL: string + CALENDAR_URL: string + REKONI_URL: string + INITIAL_URL: string + GITHUB_APP: string + GITHUB_CLIENTID: string + GITHUB_URL: string + CONFIG_URL: string + LOVE_ENDPOINT?: string + LIVEKIT_WS?: string + SIGN_URL?: string + PRINT_URL?: string + PUSH_PUBLIC_KEY: string + ANALYTICS_COLLECTOR_URL?: string + BRANDING_URL?: string + PREVIEW_CONFIG: string + DESKTOP_UPDATES_URL?: string + DESKTOP_UPDATES_CHANNEL?: string +} + +export interface Branding { + title?: string + links?: { + rel: string + href: string + type?: string + sizes?: string + }[] + languages?: string + lastNameFirst?: string + defaultLanguage?: string + defaultApplication?: string + defaultSpace?: string + defaultSpecial?: string + initWorkspace?: string +} + +export type BrandingMap = Record + +/** + * @public + */ +export interface NotificationParams { + title: string + body: string + silent: boolean +} + +/** + * @public + */ +export interface IPCMainExposed { + setBadge: (badge: number) => void + setTitle: (title: string) => void + config: () => Promise + branding: () => Promise + on: (event: string, op: (channel: any, args: any[]) => void) => void + handleDeepLink: (callback: (url: string) => void) => void + handleNotificationNavigation: (callback: () => void) => void + handleUpdateDownloadProgress: (callback: (progress: number) => void) => void + setFrontCookie: (host: string, name: string, value: string) => Promise + dockBounce: () => void + sendNotification: (notififationParams: NotificationParams) => void + getScreenAccess: () => Promise + getScreenSources: () => Promise +} diff --git a/desktop/tests/fixtures/deep-link.html b/desktop/tests/fixtures/deep-link.html new file mode 100644 index 0000000000..c5a0f77a63 --- /dev/null +++ b/desktop/tests/fixtures/deep-link.html @@ -0,0 +1,58 @@ + + + +
+

Open static url in app

+ + Open in the app + +
+
+

Open any url in app

+ + + + +
+ + \ No newline at end of file diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json new file mode 100644 index 0000000000..ae348f3bc8 --- /dev/null +++ b/desktop/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "dist", + "noImplicitAny": true, + "module": "esnext", + "target": "es2021", + "allowJs": true, + "declaration": true, + "strictNullChecks": true, + "sourceMap": true, + "skipLibCheck": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "lib": ["es2016", "dom", "ES2021.String", "ESNext.Array"] + } +} diff --git a/desktop/webpack.config.js b/desktop/webpack.config.js new file mode 100644 index 0000000000..0e33fac985 --- /dev/null +++ b/desktop/webpack.config.js @@ -0,0 +1,342 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +const Dotenv = require('dotenv-webpack') +const path = require('path') +const CompressionPlugin = require('compression-webpack-plugin') +const DefinePlugin = require('webpack').DefinePlugin +const HtmlWebpackPlugin = require('html-webpack-plugin') +const CopyPlugin = require('copy-webpack-plugin') + +const mode = process.env.NODE_ENV || 'development' +const prod = mode === 'production' +const dev = (process.env.CLIENT_TYPE ?? '') === 'dev' || mode === 'development' +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin') +console.log('mode', mode) +const { EsbuildPlugin } = require('esbuild-loader') + +const doValidate = !prod || (process.env.DO_VALIDATE === 'true') + +/** + * @type {Configuration} + */ +module.exports = [ + { + mode: dev ? 'development' : mode, + entry: { + serviceWorker: '@hcengineering/notification/src/serviceWorker.ts' + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /(node_modules|\.webpack)/, + use: { + loader: 'esbuild-loader', + options: { + target: 'es2021', + keepNames: true, + minify: !prod, + sourcemap: !prod + } + } + } + ] + }, + output: { + path: __dirname + '/dist', + filename: '[name].js', + chunkFilename: '[name].js', + publicPath: '/', + pathinfo: false + }, + resolve: { + extensions: ['.ts', '.js'], + conditionNames: ['svelte', 'browser', 'import'] + } + }, + { + mode: dev ? 'development' : mode, + entry: { + electron: './src/main/start.ts', + preload: './src/ui/preload.ts' + }, + target: 'electron-main', + plugins: [ + ...(doValidate ? [new ForkTsCheckerWebpackPlugin()] : []), + new Dotenv({ path: prod ? '.env' : '.env-dev' }), + new DefinePlugin({ + 'process.env.MODEL_VERSION': JSON.stringify(process.env.MODEL_VERSION) + }) + ], + module: { + rules: [ + // Add support for native node modules + { + // We're specifying native_modules in the test because the asset relocator loader generates a + // "fake" .node file which is really a cjs file. + test: /native_modules[/\\].+\.node$/, + use: 'node-loader' + }, + { + test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, + parser: { amd: false }, + use: { + loader: '@vercel/webpack-asset-relocator-loader', + options: { + outputAssetBase: 'native_modules' + } + } + }, + { + test: /\.ts$/, + exclude: /(node_modules|\.webpack)/, + use: { + loader: 'esbuild-loader', + options: { + target: 'es2021', + keepNames: true, + minify: !prod, + sourcemap: !prod + } + } + } + ] + }, + output: { + globalObject: 'this', + path: path.join(__dirname, '/dist/main/'), + publicPath: '', + clean: true + }, + resolve: { + extensions: ['.ts', '.js'], + conditionNames: ['svelte', 'browser', 'import'] + } + }, + + // ------ UI Part -------------------------- + { + entry: { + bundle: ['@hcengineering/theme/styles/global.scss', ...['./src/ui/index.ts']] + }, + ignoreWarnings: [ + { + message: /a11y-/ + }, + /warning from compiler/, + (warning) => true + ], + resolve: { + symlinks: true, + alias: { + svelte: path.resolve('node_modules', 'svelte/src/runtime'), + '@hcengineering/platform-rig/profiles/ui/svelte': path.resolve('node_modules', 'svelte/src/runtime') + }, + fallback: { + crypto: false, + path: false, + fs: false + }, + extensions: ['.mjs', '.js', '.svelte', '.ts'], + conditionNames: ['svelte', 'browser', 'import'] + }, + output: { + path: path.join(__dirname, '/dist/ui/'), + filename: 'bundle.js', + publicPath: '', + chunkFormat: false, + clean: true + }, + optimization: prod + ? { + minimize: true, + minimizer: [ + new EsbuildPlugin({ + target: 'es2021', + keepNames: true, + minify: !prod, + sourcemap: !prod + }) + ] + } + : {}, + module: { + rules: [ + { + test: /\.ts?$/, + loader: 'esbuild-loader', + options: { + target: 'es2021', + keepNames: true, + minify: !prod, + sourcemap: !prod + }, + exclude: /node_modules/ + }, + { + test: /\.svelte$/, + use: { + loader: 'svelte-loader', + options: { + compilerOptions: { + dev + }, + emitCss: true, + hotReload: dev, + preprocess: require('svelte-preprocess')({ + postcss: true, + sourceMap: dev + }), + hotOptions: { + // Prevent preserving local component state + preserveLocalState: true, + + // If this string appears anywhere in your component's code, then local + // state won't be preserved, even when noPreserveState is false + noPreserveStateKey: '@!hmr', + + // Prevent doing a full reload on next HMR update after fatal error + noReload: true, + + // Try to recover after runtime errors in component init + optimistic: false, + + // --- Advanced --- + + // Prevent adding an HMR accept handler to components with + // accessors option to true, or to components with named exports + // (from + +
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + diff --git a/plugins/desktop-preferences-resources/src/index.ts b/plugins/desktop-preferences-resources/src/index.ts new file mode 100644 index 0000000000..99501e91da --- /dev/null +++ b/plugins/desktop-preferences-resources/src/index.ts @@ -0,0 +1,25 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Resources } from '@hcengineering/platform' +import DesktopPreferencesPresenter from './components/DesktopPreferencesPresenter.svelte' + +export default async (): Promise => ({ + component: { + DesktopPreferencesPresenter + } +}) + +export * from './utils' diff --git a/plugins/desktop-preferences-resources/src/plugin.ts b/plugins/desktop-preferences-resources/src/plugin.ts new file mode 100644 index 0000000000..1bdf55b1f6 --- /dev/null +++ b/plugins/desktop-preferences-resources/src/plugin.ts @@ -0,0 +1,19 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { mergeIds } from '@hcengineering/platform' +import desktopPreferences, { desktopPreferencesId } from '@hcengineering/desktop-preferences' + +export default mergeIds(desktopPreferencesId, desktopPreferences, {}) diff --git a/plugins/desktop-preferences-resources/src/utils.ts b/plugins/desktop-preferences-resources/src/utils.ts new file mode 100644 index 0000000000..5a2334fd55 --- /dev/null +++ b/plugins/desktop-preferences-resources/src/utils.ts @@ -0,0 +1,28 @@ +import { writable } from 'svelte/store' + +import { createQuery } from '@hcengineering/presentation' +import desktopPreferences, { + defaultNotificationPreference, + type DesktopNotificationPreference, + type DesktopNotificationPreferenceData +} from '@hcengineering/desktop-preferences' + +// /** +// * @public +// */ +export const activePreferences = writable( + defaultNotificationPreference +) + +const preferencesQuery = createQuery(true) +preferencesQuery.query( + desktopPreferences.class.DesktopNotificationPreference, + {}, + (res: DesktopNotificationPreference[]) => { + if (res?.length > 0) { + activePreferences.set(res[0]) + } else { + activePreferences.set(defaultNotificationPreference) + } + } +) diff --git a/plugins/desktop-preferences-resources/svelte.config.js b/plugins/desktop-preferences-resources/svelte.config.js new file mode 100644 index 0000000000..944a06f73e --- /dev/null +++ b/plugins/desktop-preferences-resources/svelte.config.js @@ -0,0 +1,5 @@ +const sveltePreprocess = require('svelte-preprocess') + +module.exports = { + preprocess: sveltePreprocess() +}; \ No newline at end of file diff --git a/plugins/desktop-preferences-resources/tsconfig.json b/plugins/desktop-preferences-resources/tsconfig.json new file mode 100644 index 0000000000..fb6ccb2d18 --- /dev/null +++ b/plugins/desktop-preferences-resources/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types" + } +} \ No newline at end of file diff --git a/plugins/desktop-preferences/.eslintrc.js b/plugins/desktop-preferences/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/plugins/desktop-preferences/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/plugins/desktop-preferences/.npmignore b/plugins/desktop-preferences/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/plugins/desktop-preferences/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/plugins/desktop-preferences/config/rig.json b/plugins/desktop-preferences/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/plugins/desktop-preferences/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/plugins/desktop-preferences/jest.config.js b/plugins/desktop-preferences/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/plugins/desktop-preferences/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/plugins/desktop-preferences/package.json b/plugins/desktop-preferences/package.json new file mode 100644 index 0000000000..8e09db09f7 --- /dev/null +++ b/plugins/desktop-preferences/package.json @@ -0,0 +1,45 @@ +{ + "name": "@hcengineering/desktop-preferences", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5" + }, + "dependencies": { + "@hcengineering/ui": "^0.6.15", + "@hcengineering/core": "^0.6.32", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/preference": "^0.6.13", + "@hcengineering/notification": "^0.6.23" + } +} diff --git a/plugins/desktop-preferences/src/index.ts b/plugins/desktop-preferences/src/index.ts new file mode 100644 index 0000000000..8393c71fa3 --- /dev/null +++ b/plugins/desktop-preferences/src/index.ts @@ -0,0 +1,80 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Ref, Class, Data } from '@hcengineering/core' +import { Preference } from '@hcengineering/preference' +import { NotificationPreferencesGroup } from '@hcengineering/notification' +import type { Asset, IntlString, Plugin } from '@hcengineering/platform' +import { plugin } from '@hcengineering/platform' +import { AnyComponent } from '@hcengineering/ui' + +/** + * @public + */ +export interface DesktopNotificationPreference extends Preference { + showNotifications: boolean + playSound: boolean + bounceAppIcon: boolean + showUnreadCounter: boolean +} + +/** + * @public + */ +export type DesktopNotificationPreferenceData = Omit, 'attachedTo'> + +/** + * @public + */ +export type PreferenceKey = keyof DesktopNotificationPreferenceData + +/** + * @public + */ +export const defaultNotificationPreference: DesktopNotificationPreferenceData = { + showNotifications: true, + playSound: false, + bounceAppIcon: false, + showUnreadCounter: true +} + +/** + * @public + */ +export const desktopPreferencesId = 'desktop-preferences' as Plugin + +export default plugin(desktopPreferencesId, { + class: { + DesktopNotificationPreference: '' as Ref> + }, + component: { + DesktopPreferencesPresenter: '' as AnyComponent + }, + icon: { + NotificationMessage: '' as Asset + }, + string: { + ShowNotifications: '' as IntlString, + SoundAndAppearance: '' as IntlString, + PlaySound: '' as IntlString, + BounceAppIcon: '' as IntlString, + ShowBadge: '' as IntlString, + HaveGotANotification: '' as IntlString, + TotalNotificationsCount: '' as IntlString + }, + ids: { + DesktopNotificationPreferencesGroup: '' as Ref + } +}) diff --git a/plugins/desktop-preferences/tsconfig.json b/plugins/desktop-preferences/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/plugins/desktop-preferences/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/plugins/diffview-assets/.eslintrc.js b/plugins/diffview-assets/.eslintrc.js new file mode 100644 index 0000000000..e73094bc7e --- /dev/null +++ b/plugins/diffview-assets/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/assets/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/plugins/diffview-assets/assets/icons.svg b/plugins/diffview-assets/assets/icons.svg new file mode 100644 index 0000000000..9ebf0a90ba --- /dev/null +++ b/plugins/diffview-assets/assets/icons.svg @@ -0,0 +1,2 @@ + + diff --git a/plugins/diffview-assets/config/rig.json b/plugins/diffview-assets/config/rig.json new file mode 100644 index 0000000000..b75800b9b7 --- /dev/null +++ b/plugins/diffview-assets/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "assets" +} diff --git a/plugins/diffview-assets/jest.config.js b/plugins/diffview-assets/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/plugins/diffview-assets/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/plugins/diffview-assets/lang/en.json b/plugins/diffview-assets/lang/en.json new file mode 100644 index 0000000000..56e1b9e970 --- /dev/null +++ b/plugins/diffview-assets/lang/en.json @@ -0,0 +1,14 @@ +{ + "string": { + "ViewMode": "View mode", + "Unified": "Unified", + "Split": "Split", + "Viewed": "Viewed", + "ShowDiff": "Show diff", + "NoChanges": "File diff has no changes", + "LargeDiffsAreHidden": "Large diffs are not rendered by default.", + "FileIsTooLarge": "File is too large to be displayed.", + "FileWasRenamed": "File was renamed without changes.", + "FileWasDeleted": "File was deleted." + } +} diff --git a/plugins/diffview-assets/lang/ru.json b/plugins/diffview-assets/lang/ru.json new file mode 100644 index 0000000000..49b8a9a5ed --- /dev/null +++ b/plugins/diffview-assets/lang/ru.json @@ -0,0 +1,14 @@ +{ + "string": { + "ViewMode": "Режим представления", + "Unified": "Совместный", + "Split": "Раздельный", + "Viewed": "Просмотрено", + "ShowDiff": "Показать изменения", + "NoChanges": "Нет изменений", + "LargeDiffsAreHidden": "Большие изменения не отображаются по-умолчанию.", + "FileIsTooLarge": "Файл слишком большой для отображения.", + "FileWasRenamed": "Файл был переименован без изменений.", + "FileWasDeleted": "Файл был удалён." + } +} diff --git a/plugins/diffview-assets/package.json b/plugins/diffview-assets/package.json new file mode 100644 index 0000000000..01bbb4ae20 --- /dev/null +++ b/plugins/diffview-assets/package.json @@ -0,0 +1,39 @@ +{ + "name": "@hcengineering/diffview-assets", + "version": "0.6.0", + "main": "src/index.ts", + "author": "Copyright © Hardcore Engineering Inc.", + "template": "@hcengineering/assets-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "test": "jest --passWithNoTests --silent", + "build:docs": "", + "format": "format src", + "build:watch": "compile", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "eslint": "^8.54.0", + "prettier": "^3.1.0", + "@types/node": "~20.11.16", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/platform": "^0.6.11", + "@hcengineering/diffview": "^0.6.0" + } +} diff --git a/plugins/diffview-assets/src/__tests__/lang.test.ts b/plugins/diffview-assets/src/__tests__/lang.test.ts new file mode 100644 index 0000000000..cf96c6dcf7 --- /dev/null +++ b/plugins/diffview-assets/src/__tests__/lang.test.ts @@ -0,0 +1,6 @@ +import { makeLocalesTest } from '@hcengineering/platform' + +it( + 'Locales are equale', + makeLocalesTest((lang) => import(`../../lang/${lang}.json`)) +) diff --git a/plugins/diffview-assets/src/index.ts b/plugins/diffview-assets/src/index.ts new file mode 100644 index 0000000000..2cd85bad20 --- /dev/null +++ b/plugins/diffview-assets/src/index.ts @@ -0,0 +1,14 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// diff --git a/plugins/diffview-assets/tsconfig.json b/plugins/diffview-assets/tsconfig.json new file mode 100644 index 0000000000..4449c73536 --- /dev/null +++ b/plugins/diffview-assets/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/assets/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "types": ["node", "jest"], + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/plugins/diffview-resources/.eslintrc.js b/plugins/diffview-resources/.eslintrc.js new file mode 100644 index 0000000000..bb8fd7450d --- /dev/null +++ b/plugins/diffview-resources/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'], + parserOptions: { tsconfigRootDir: __dirname } +} diff --git a/plugins/diffview-resources/.prettierrc b/plugins/diffview-resources/.prettierrc new file mode 100644 index 0000000000..792942803a --- /dev/null +++ b/plugins/diffview-resources/.prettierrc @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "trailingComma": "none", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 120, + "useTabs": false, + "bracketSpacing": true, + "proseWrap": "preserve", + "plugins": [ + "prettier-plugin-svelte" + ], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} \ No newline at end of file diff --git a/plugins/diffview-resources/config/rig.json b/plugins/diffview-resources/config/rig.json new file mode 100644 index 0000000000..bcad6f7c33 --- /dev/null +++ b/plugins/diffview-resources/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "ui" +} diff --git a/plugins/diffview-resources/jest.config.js b/plugins/diffview-resources/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/plugins/diffview-resources/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/plugins/diffview-resources/package.json b/plugins/diffview-resources/package.json new file mode 100644 index 0000000000..b511bb4fcd --- /dev/null +++ b/plugins/diffview-resources/package.json @@ -0,0 +1,53 @@ +{ + "name": "@hcengineering/diffview-resources", + "version": "0.6.0", + "main": "src/index.ts", + "author": "Copyright © Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "build": "compile ui", + "build:docs": "api-extractor run --local", + "format": "format src", + "svelte-check": "do-svelte-check", + "_phase:svelte-check": "do-svelte-check", + "build:watch": "compile ui", + "_phase:build": "compile ui", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "svelte-loader": "^3.2.0", + "sass": "^1.53.0", + "svelte-preprocess": "^5.1.3", + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "prettier-plugin-svelte": "^3.2.2", + "eslint": "^8.54.0", + "prettier": "^3.1.0", + "svelte-check": "^3.6.9", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "eslint-plugin-svelte": "^2.35.1", + "svelte-eslint-parser": "^0.33.1" + }, + "dependencies": { + "svelte": "^4.2.12", + "@hcengineering/core": "^0.6.32", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/presentation": "^0.6.3", + "@hcengineering/ui": "^0.6.15", + "@hcengineering/view": "^0.6.13", + "@hcengineering/view-resources": "^0.6.0", + "@hcengineering/diffview": "^0.6.0", + "fast-equals": "^5.0.1", + "highlight.js": "~11.8.0", + "diff2html": "~3.4.35" + } +} diff --git a/plugins/diffview-resources/postcss.config.js b/plugins/diffview-resources/postcss.config.js new file mode 100644 index 0000000000..88752c6cb0 --- /dev/null +++ b/plugins/diffview-resources/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + require('autoprefixer') + ] +} diff --git a/plugins/diffview-resources/src/components/DiffView.svelte b/plugins/diffview-resources/src/components/DiffView.svelte new file mode 100644 index 0000000000..5072f744ca --- /dev/null +++ b/plugins/diffview-resources/src/components/DiffView.svelte @@ -0,0 +1,62 @@ + + + +
+ + { + saveMode(detail) + }} + /> +
+ +{#each diffFiles as diffFile} + {@const fileViewed = isFileViewed(diffFile)} + +{/each} diff --git a/plugins/diffview-resources/src/components/DiffViewModeDropdown.svelte b/plugins/diffview-resources/src/components/DiffViewModeDropdown.svelte new file mode 100644 index 0000000000..d72c353e09 --- /dev/null +++ b/plugins/diffview-resources/src/components/DiffViewModeDropdown.svelte @@ -0,0 +1,38 @@ + + + + diff --git a/plugins/diffview-resources/src/components/FileDiffContent.svelte b/plugins/diffview-resources/src/components/FileDiffContent.svelte new file mode 100644 index 0000000000..02664d4fae --- /dev/null +++ b/plugins/diffview-resources/src/components/FileDiffContent.svelte @@ -0,0 +1,228 @@ + + + +
+ {#if highlighted === undefined} + + {:else} + + + {#if mode === 'unified'} + + + + {:else} + + + + + {/if} + + + {#each highlighted as hhunk} + {@const hunk = hhunk.hunk} + + {#if mode === 'unified'} + {@const lines = prepareLinesForUnifiedView(hhunk.lines)} + + + + + {#each lines as line} + {@const lineClass = codeLineClass(line)} + + + + + + {/each} + {:else} + {@const lines = prepareLinesForSplitView(hhunk.lines)} + + + + + {#each lines as line} + {@const before = line.before} + {@const after = line.after} + + {@const beforeLineClass = codeLineClass(before)} + {@const afterLineClass = codeLineClass(after)} + + + + + + + {/each} + {/if} + {/each} + +
+ + {hunk.header}
{line.oldNumber ?? ''}{line.newNumber ?? ''} + {@html line.content} +
+ {hunk.header}
{before.oldNumber ?? ''} + {@html before.content} + {after.newNumber ?? ''} + {@html after.content} +
+ {/if} +
+ + diff --git a/plugins/diffview-resources/src/components/FileDiffHeader.svelte b/plugins/diffview-resources/src/components/FileDiffHeader.svelte new file mode 100644 index 0000000000..d74c450ff9 --- /dev/null +++ b/plugins/diffview-resources/src/components/FileDiffHeader.svelte @@ -0,0 +1,121 @@ + + + +
+ +
+ {#if showExpand} + + {/if} + +
+ {formatFileName(file)} +
+ + +
copyFileNameToClipboard()}> + +
+
+ +
+ {#if showChanges} +
+
+ + + {file.stats.addedLines} +
+
+ + + {file.stats.deletedLines} +
+
+ {/if} + + {#if showViewed} +
+
+ {/if} +
+
+ + diff --git a/plugins/diffview-resources/src/components/FileDiffView.svelte b/plugins/diffview-resources/src/components/FileDiffView.svelte new file mode 100644 index 0000000000..e90d262a31 --- /dev/null +++ b/plugins/diffview-resources/src/components/FileDiffView.svelte @@ -0,0 +1,151 @@ + + + +
+ +
+ { + expanded = !expanded + dispatch('expanded', expanded) + }} + on:viewed={(evt) => { + viewed = evt.detail + expanded = !viewed + dispatch('change', { fileName: file.fileName, sha: file.sha, viewed }) + }} + /> +
+ + + {#if expanded} +
+ {#if file.hunks.length === 0} + {@const label = getNoChangesLabel(file)} +
+ +
+ {/if} + {#if hideDiffLabel} +
+
+ {:else} + + {/if} +
+ {/if} +
+ + diff --git a/plugins/diffview-resources/src/components/InlineDiffView.svelte b/plugins/diffview-resources/src/components/InlineDiffView.svelte new file mode 100644 index 0000000000..a3862e8fd1 --- /dev/null +++ b/plugins/diffview-resources/src/components/InlineDiffView.svelte @@ -0,0 +1,48 @@ + + + +{#each diffFiles as diffFile} + { + onExpand?.(evt.detail) + }} + /> +{/each} diff --git a/plugins/diffview-resources/src/components/theme/github-dark.scss b/plugins/diffview-resources/src/components/theme/github-dark.scss new file mode 100644 index 0000000000..ec02d58fa3 --- /dev/null +++ b/plugins/diffview-resources/src/components/theme/github-dark.scss @@ -0,0 +1,94 @@ +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em; +} +code.hljs { + padding: 3px 5px; +} /*! + Theme: GitHub Dark + Description: Dark theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-dark + Current colors taken from GitHub's CSS +*/ +.hljs { + color: #c9d1d9; + // background: #0d1117; +} +.hljs-doctag, +.hljs-keyword, +.hljs-meta .hljs-keyword, +.hljs-template-tag, +.hljs-template-variable, +.hljs-type, +.hljs-variable.language_ { + color: #ff7b72; +} +.hljs-title, +.hljs-title.class_, +.hljs-title.class_.inherited__, +.hljs-title.function_ { + color: #d2a8ff; +} +.hljs-attr, +.hljs-attribute, +.hljs-literal, +.hljs-meta, +.hljs-number, +.hljs-operator, +.hljs-selector-attr, +.hljs-selector-class, +.hljs-selector-id, +.hljs-variable { + color: #79c0ff; +} +.hljs-meta .hljs-string, +.hljs-regexp, +.hljs-string { + color: #a5d6ff; +} +.hljs-built_in, +.hljs-symbol { + color: #ffa657; +} +.hljs-code, +.hljs-comment, +.hljs-formula { + color: #8b949e; +} +.hljs-name, +.hljs-quote, +.hljs-selector-pseudo, +.hljs-selector-tag { + color: #7ee787; +} +.hljs-subst { + color: #c9d1d9; +} +.hljs-section { + color: #1f6feb; + font-weight: 700; +} +.hljs-bullet { + color: #f2cc60; +} +.hljs-emphasis { + color: #c9d1d9; + font-style: italic; +} +.hljs-strong { + color: #c9d1d9; + font-weight: 700; +} +.hljs-addition { + color: #aff5b4; + background-color: #033a16; +} +.hljs-deletion { + color: #ffdcd7; + background-color: #67060c; +} diff --git a/plugins/diffview-resources/src/components/theme/github.scss b/plugins/diffview-resources/src/components/theme/github.scss new file mode 100644 index 0000000000..56c08082b0 --- /dev/null +++ b/plugins/diffview-resources/src/components/theme/github.scss @@ -0,0 +1,94 @@ +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em; +} +code.hljs { + padding: 3px 5px; +} /*! + Theme: GitHub + Description: Light theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-light + Current colors taken from GitHub's CSS +*/ +.hljs { + color: #24292e; + // background: #fff; +} +.hljs-doctag, +.hljs-keyword, +.hljs-meta .hljs-keyword, +.hljs-template-tag, +.hljs-template-variable, +.hljs-type, +.hljs-variable.language_ { + color: #d73a49; +} +.hljs-title, +.hljs-title.class_, +.hljs-title.class_.inherited__, +.hljs-title.function_ { + color: #6f42c1; +} +.hljs-attr, +.hljs-attribute, +.hljs-literal, +.hljs-meta, +.hljs-number, +.hljs-operator, +.hljs-selector-attr, +.hljs-selector-class, +.hljs-selector-id, +.hljs-variable { + color: #005cc5; +} +.hljs-meta .hljs-string, +.hljs-regexp, +.hljs-string { + color: #032f62; +} +.hljs-built_in, +.hljs-symbol { + color: #e36209; +} +.hljs-code, +.hljs-comment, +.hljs-formula { + color: #6a737d; +} +.hljs-name, +.hljs-quote, +.hljs-selector-pseudo, +.hljs-selector-tag { + color: #22863a; +} +.hljs-subst { + color: #24292e; +} +.hljs-section { + color: #005cc5; + font-weight: 700; +} +.hljs-bullet { + color: #735c0f; +} +.hljs-emphasis { + color: #24292e; + font-style: italic; +} +.hljs-strong { + color: #24292e; + font-weight: 700; +} +.hljs-addition { + color: #22863a; + background-color: #f0fff4; +} +.hljs-deletion { + color: #b31d28; + background-color: #ffeef0; +} diff --git a/plugins/diffview-resources/src/highlight/highlight.ts b/plugins/diffview-resources/src/highlight/highlight.ts new file mode 100644 index 0000000000..96c37bca6c --- /dev/null +++ b/plugins/diffview-resources/src/highlight/highlight.ts @@ -0,0 +1,59 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import hljs from 'highlight.js' +import { hljsDefineSvelte } from './languages/svelte-hljs' + +hljs.registerLanguage('svelte', hljsDefineSvelte) + +export interface HighlightOptions { + language: string +} + +export function highlightText (text: string, options: HighlightOptions): string { + // We should always use highlighter because it sanitizes the input + // We have to always use highlighter to ensure that the input is sanitized + const validLanguage = options.language !== '' && hljs.getLanguage(options.language) !== undefined + const language = validLanguage ? options.language : 'text' + + const { value: highlighted } = hljs.highlight(text, { language }) + const normalized = normalizeHighlightTags(highlighted) + return normalized +} + +export function highlightLines (lines: string[], options: HighlightOptions): string[] { + const highlighted = highlightText(lines.join('\n'), options) + return highlighted.split('\n') +} + +function normalizeHighlightTags (highlighted: string): string { + const openTags: string[] = [] + + const normalized = highlighted.replace(/(]*>)|(<\/span>)|(\n)/g, (match) => { + if (match === '\n') { + return ''.repeat(openTags.length) + '\n' + openTags.join('') + } + + if (match === '') { + openTags.pop() + } else { + openTags.push(match) + } + + return match + }) + + return normalized +} diff --git a/plugins/diffview-resources/src/highlight/index.ts b/plugins/diffview-resources/src/highlight/index.ts new file mode 100644 index 0000000000..c8a9cdde3b --- /dev/null +++ b/plugins/diffview-resources/src/highlight/index.ts @@ -0,0 +1,17 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './highlight' +export * from './render' diff --git a/plugins/diffview-resources/src/highlight/languages/svelte-hljs.ts b/plugins/diffview-resources/src/highlight/languages/svelte-hljs.ts new file mode 100644 index 0000000000..a47af00f5d --- /dev/null +++ b/plugins/diffview-resources/src/highlight/languages/svelte-hljs.ts @@ -0,0 +1,48 @@ +export function hljsDefineSvelte (hljs: any): any { + return { + subLanguage: 'xml', + contains: [ + hljs.COMMENT('', { + relevance: 10 + }), + { + begin: /^(\s*)()/gm, + end: /^(\s*)(<\/script>)/gm, + subLanguage: 'javascript', + excludeBegin: true, + excludeEnd: true, + contains: [ + { + begin: /^(\s*)(\$:)/gm, + end: /(\s*)/gm, + className: 'keyword' + } + ] + }, + { + begin: /^(\s*)()/gm, + end: /^(\s*)(<\/style>)/gm, + subLanguage: 'css', + excludeBegin: true, + excludeEnd: true + }, + { + begin: /\{/gm, + end: /\}/gm, + subLanguage: 'javascript', + contains: [ + { + begin: /[{]/, + end: /[}]/, + skip: true + }, + { + begin: /([#:/@])(if|else|each|await|then|catch|debug|html)/gm, + className: 'keyword', + relevance: 10 + } + ] + } + ] + } +} diff --git a/plugins/diffview-resources/src/highlight/render.ts b/plugins/diffview-resources/src/highlight/render.ts new file mode 100644 index 0000000000..6b2905d572 --- /dev/null +++ b/plugins/diffview-resources/src/highlight/render.ts @@ -0,0 +1,86 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type DiffHunk, type DiffLine, DiffLineType, EmptyLine } from '@hcengineering/diffview' +import { type HighlightOptions, highlightLines } from './highlight' + +export interface RenderOptions { + syntaxHighlight: { + language: string + } +} + +export interface DiffLineRenderResult { + before: DiffLine + after: DiffLine +} + +export interface DiffHunkRenderResult { + hunk: DiffHunk + lines: DiffLineRenderResult[] +} + +export function renderHunk (hunk: DiffHunk, options: RenderOptions): DiffHunkRenderResult { + const syntaxHighlight = options.syntaxHighlight + + const highlightOptions: HighlightOptions = { language: syntaxHighlight.language } + + const { before, after } = splitDiffLines(hunk.lines) + const beforeHighlighted = highlightDiffLines(before, highlightOptions) + const afterHighlighted = highlightDiffLines(after, highlightOptions) + + const lines: DiffLineRenderResult[] = [] + while (beforeHighlighted.length > 0 || afterHighlighted.length > 0) { + const beforeLine = beforeHighlighted.shift() ?? EmptyLine + const afterLine = afterHighlighted.shift() ?? EmptyLine + lines.push({ before: beforeLine, after: afterLine }) + } + + return { hunk, lines } +} + +function splitDiffLines (lines: DiffLine[]): { before: DiffLine[], after: DiffLine[] } { + const before: DiffLine[] = [] + const after: DiffLine[] = [] + + for (const line of lines) { + if (line.type === DiffLineType.CONTEXT) { + before.push(line) + after.push(line) + } else if (line.type === DiffLineType.DELETE) { + before.push(line) + after.push(EmptyLine) + } else if (line.type === DiffLineType.INSERT) { + before.push(EmptyLine) + after.push(line) + } else { + before.push(line) + after.push(line) + } + } + + return { before, after } +} + +function highlightDiffLines (lines: DiffLine[], options: HighlightOptions): DiffLine[] { + // Highlight entire diff hunk content, it is more accurate than highlighting line-by-line + const content = lines.filter((line) => line.type !== DiffLineType.EMPTY).map((line) => line.content) + const highlighted = highlightLines(content, options) + + // Reconstruct DiffLine items with highlighted content + return lines.map((line) => + line.type === DiffLineType.EMPTY ? { ...line } : { ...line, content: highlighted.shift() ?? '' } + ) +} diff --git a/plugins/diffview-resources/src/index.ts b/plugins/diffview-resources/src/index.ts new file mode 100644 index 0000000000..4d1c1636e6 --- /dev/null +++ b/plugins/diffview-resources/src/index.ts @@ -0,0 +1,24 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Resources } from '@hcengineering/platform' +import DiffView from './components/DiffView.svelte' +import InlineDiffView from './components/InlineDiffView.svelte' +export default async (): Promise => ({ + component: { + DiffView, + InlineDiffView + } +}) diff --git a/plugins/diffview-resources/src/parser.ts b/plugins/diffview-resources/src/parser.ts new file mode 100644 index 0000000000..c94c82d8f5 --- /dev/null +++ b/plugins/diffview-resources/src/parser.ts @@ -0,0 +1,103 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Diff2HtmlConfig, parse } from 'diff2html' +import { + type DiffBlock as D2HDiffBlock, + type DiffFile as D2HDiffFile, + type DiffLine as D2HDiffLine +} from 'diff2html/lib/types' +import { + type Diff, + type DiffFile, + type DiffFileType, + type DiffHunk, + type DiffLine, + DiffLineType +} from '@hcengineering/diffview' +import { isDevNullName } from './utils' + +const diff2htmlConfig: Diff2HtmlConfig = { + diffMaxChanges: 10000 +} + +/** + * @public + */ +export function parseDiff (diff: Diff): DiffFile[] { + const files = parse(diff, diff2htmlConfig) + return files.map(mapFile) +} + +function mapFile (file: D2HDiffFile): DiffFile { + const { language, oldName, newName, addedLines, deletedLines, isBinary, isTooBig } = file + + const fileName = isDevNullName(newName) ? oldName : newName + const sha = file.checksumAfter ?? '' + + const diffType = mapFileDiffType(file) + const stats = { addedLines, deletedLines } + const hunks = isTooBig === true ? [] : file.blocks.map(mapHunk) + + return { hunks, language, oldName, newName, fileName, sha, isBinary, isTooBig, diffType, stats } +} + +function mapHunk (block: D2HDiffBlock): DiffHunk { + const { header, oldStartLine, newStartLine } = block + const lines = block.lines.map(mapLine) + + return { header, oldStartLine, newStartLine, lines } +} + +function mapLine (line: D2HDiffLine): DiffLine { + const { type, oldNumber, newNumber } = line + const { prefix, content } = parseContentLine(line.content) + + switch (type) { + case 'context': + return { type: DiffLineType.CONTEXT, oldNumber, newNumber, prefix, content } + case 'insert': + return { type: DiffLineType.INSERT, oldNumber: undefined, newNumber, prefix, content } + case 'delete': + return { type: DiffLineType.DELETE, oldNumber, newNumber: undefined, prefix, content } + default: + throw new Error(`Unexpected line type: ${type}`) + } +} + +function mapFileDiffType (file: D2HDiffFile): DiffFileType { + if (file.isNew === true) { + return 'add' + } + if (file.isDeleted === true) { + return 'delete' + } + if (file.isRename === true) { + return 'rename' + } + if (file.isCopy === true) { + return 'copy' + } + return 'modify' +} + +const prefixLength = 1 + +function parseContentLine (line: string): { prefix: string, content: string } { + return { + prefix: line.substring(0, prefixLength), + content: line.substring(prefixLength) + } +} diff --git a/plugins/diffview-resources/src/plugin.ts b/plugins/diffview-resources/src/plugin.ts new file mode 100644 index 0000000000..4b2fcbc754 --- /dev/null +++ b/plugins/diffview-resources/src/plugin.ts @@ -0,0 +1,28 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type IntlString, mergeIds } from '@hcengineering/platform' +import diffview, { diffviewId } from '@hcengineering/diffview' + +export default mergeIds(diffviewId, diffview, { + string: { + ShowDiff: '' as IntlString, + LargeDiffsAreHidden: '' as IntlString, + NoChanges: '' as IntlString, + FileIsTooLarge: '' as IntlString, + FileWasRenamed: '' as IntlString, + FileWasDeleted: '' as IntlString + } +}) diff --git a/plugins/diffview-resources/src/utils.ts b/plugins/diffview-resources/src/utils.ts new file mode 100644 index 0000000000..a6840dcb70 --- /dev/null +++ b/plugins/diffview-resources/src/utils.ts @@ -0,0 +1,37 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type DiffFile } from '@hcengineering/diffview' + +export function isDevNullName (name: string): boolean { + return name === '/dev/null' +} + +export function getFileName (file: DiffFile): string { + const { oldName, newName } = file + return isDevNullName(newName) ? oldName : newName +} + +export function formatFileName (file: DiffFile): string { + const { oldName, newName } = file + + if (oldName !== newName && !isDevNullName(oldName) && !isDevNullName(newName)) { + return oldName + ' → ' + newName + } else if (!isDevNullName(newName)) { + return newName + } else { + return oldName + } +} diff --git a/plugins/diffview-resources/svelte.config.js b/plugins/diffview-resources/svelte.config.js new file mode 100644 index 0000000000..944a06f73e --- /dev/null +++ b/plugins/diffview-resources/svelte.config.js @@ -0,0 +1,5 @@ +const sveltePreprocess = require('svelte-preprocess') + +module.exports = { + preprocess: sveltePreprocess() +}; \ No newline at end of file diff --git a/plugins/diffview-resources/tsconfig.json b/plugins/diffview-resources/tsconfig.json new file mode 100644 index 0000000000..fb6ccb2d18 --- /dev/null +++ b/plugins/diffview-resources/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types" + } +} \ No newline at end of file diff --git a/plugins/diffview/.eslintrc.js b/plugins/diffview/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/plugins/diffview/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/plugins/diffview/.npmignore b/plugins/diffview/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/plugins/diffview/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/plugins/diffview/config/rig.json b/plugins/diffview/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/plugins/diffview/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/plugins/diffview/jest.config.js b/plugins/diffview/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/plugins/diffview/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/plugins/diffview/package.json b/plugins/diffview/package.json new file mode 100644 index 0000000000..3c28cc1734 --- /dev/null +++ b/plugins/diffview/package.json @@ -0,0 +1,44 @@ +{ + "name": "@hcengineering/diffview", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Copyright © Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5" + }, + "dependencies": { + "@hcengineering/core": "^0.6.32", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/ui": "^0.6.15" + } +} diff --git a/plugins/diffview/src/index.ts b/plugins/diffview/src/index.ts new file mode 100644 index 0000000000..f7be7e7c07 --- /dev/null +++ b/plugins/diffview/src/index.ts @@ -0,0 +1,59 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { IntlString, Plugin } from '@hcengineering/platform' +import { plugin } from '@hcengineering/platform' +import { AnyComponent } from '@hcengineering/ui' + +export * from './types' + +/** + * @public + */ +export const diffviewId = 'diffview' as Plugin + +/** + * @public + */ +export type Diff = string + +/** + * @public + */ +export type DiffViewMode = 'unified' | 'split' + +/** + * @public + */ +export interface DiffFileId { + fileName: string + sha: string +} + +/** + * @public + */ +export default plugin(diffviewId, { + component: { + DiffView: '' as AnyComponent, + InlineDiffView: '' as AnyComponent + }, + string: { + ViewMode: '' as IntlString, + Unified: '' as IntlString, + Split: '' as IntlString, + Viewed: '' as IntlString + } +}) diff --git a/plugins/diffview/src/types.ts b/plugins/diffview/src/types.ts new file mode 100644 index 0000000000..fe90f2233d --- /dev/null +++ b/plugins/diffview/src/types.ts @@ -0,0 +1,123 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/** + * @public + */ +export enum DiffLineType { + INSERT = 'insert', + DELETE = 'delete', + CONTEXT = 'context', + EMPTY = 'empty' +} + +/** + * @public + */ +export interface DiffLineDeleted { + type: DiffLineType.DELETE + oldNumber: number + newNumber: undefined +} + +/** + * @public + */ +export interface DiffLineInserted { + type: DiffLineType.INSERT + oldNumber: undefined + newNumber: number +} + +/** + * @public + */ +export interface DiffLineContext { + type: DiffLineType.CONTEXT + oldNumber: number + newNumber: number +} + +/** + * @public + */ +export interface DiffLineEmpty { + type: DiffLineType.EMPTY + oldNumber: undefined + newNumber: undefined +} + +/** + * @public + */ +export interface DiffLineContent { + prefix: string + content: string +} + +/** + * @public + */ +export type DiffLine = (DiffLineDeleted | DiffLineInserted | DiffLineContext | DiffLineEmpty) & DiffLineContent + +/** + * @public + */ +export const EmptyLine: DiffLine = { + type: DiffLineType.EMPTY, + oldNumber: undefined, + newNumber: undefined, + prefix: '', + content: '' +} + +/** + * @public + */ +export interface DiffHunk { + header: string + lines: DiffLine[] + oldStartLine: number + newStartLine: number +} + +/** + * @public + */ +export interface DiffFileStats { + addedLines: number + deletedLines: number +} + +/** + * @public + */ +export type DiffFileType = 'add' | 'delete' | 'modify' | 'rename' | 'copy' + +/** + * @public + */ +export interface DiffFile { + fileName: string + sha: string + hunks: DiffHunk[] + diffType: DiffFileType + language: string + oldName: string + newName: string + isBinary?: boolean + isTooBig?: boolean + stats: DiffFileStats +} diff --git a/plugins/diffview/tsconfig.json b/plugins/diffview/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/plugins/diffview/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/plugins/love-assets/package.json b/plugins/love-assets/package.json index 457b725152..532a34ae5d 100644 --- a/plugins/love-assets/package.json +++ b/plugins/love-assets/package.json @@ -2,7 +2,7 @@ "name": "@hcengineering/love-assets", "version": "0.6.0", "main": "src/index.ts", - "author": "Uberflow Contributors", + "author": "Copyright © Hardcore Engineering Inc.", "template": "@hcengineering/assets-package", "license": "EPL-2.0", "scripts": { diff --git a/plugins/love/package.json b/plugins/love/package.json index 62ce97b910..8b214191fb 100644 --- a/plugins/love/package.json +++ b/plugins/love/package.json @@ -9,7 +9,7 @@ "types/**/*", "tsconfig.json" ], - "author": "Uberflow Contributors", + "author": "Copyright © Hardcore Engineering Inc.", "license": "EPL-2.0", "scripts": { "build": "compile", diff --git a/plugins/print-assets/package.json b/plugins/print-assets/package.json index 37c14f87ba..5dd07911c5 100644 --- a/plugins/print-assets/package.json +++ b/plugins/print-assets/package.json @@ -2,7 +2,7 @@ "name": "@hcengineering/print-assets", "version": "0.6.0", "main": "src/index.ts", - "author": "Uberflow Contributors", + "author": "Copyright © Hardcore Engineering Inc.", "template": "@hcengineering/assets-package", "license": "EPL-2.0", "scripts": { diff --git a/plugins/print/package.json b/plugins/print/package.json index 1e20a0190f..265833f6d3 100644 --- a/plugins/print/package.json +++ b/plugins/print/package.json @@ -9,7 +9,7 @@ "types/**/*", "tsconfig.json" ], - "author": "Uberflow Contributors", + "author": "Copyright © Hardcore Engineering Inc.", "license": "EPL-2.0", "scripts": { "build": "compile", diff --git a/plugins/sign/package.json b/plugins/sign/package.json index 0f86373539..768074d28b 100644 --- a/plugins/sign/package.json +++ b/plugins/sign/package.json @@ -9,7 +9,7 @@ "types/**/*", "tsconfig.json" ], - "author": "Uberflow Contributors", + "author": "Copyright © Hardcore Engineering Inc.", "license": "EPL-2.0", "scripts": { "build": "compile", diff --git a/plugins/time-assets/package.json b/plugins/time-assets/package.json index 6526d041a2..d06a1f7dde 100644 --- a/plugins/time-assets/package.json +++ b/plugins/time-assets/package.json @@ -2,7 +2,7 @@ "name": "@hcengineering/time-assets", "version": "0.6.0", "main": "src/index.ts", - "author": "Uberflow Contributors", + "author": "Copyright © Hardcore Engineering Inc.", "template": "@hcengineering/assets-package", "license": "EPL-2.0", "scripts": { diff --git a/plugins/time-resources/package.json b/plugins/time-resources/package.json index 4f52fb967f..20709b519f 100644 --- a/plugins/time-resources/package.json +++ b/plugins/time-resources/package.json @@ -2,7 +2,7 @@ "name": "@hcengineering/time-resources", "version": "0.6.0", "main": "src/index.ts", - "author": "Uberflow Contributors", + "author": "Copyright © Hardcore Engineering Inc.", "license": "EPL-2.0", "scripts": { "build": "compile ui", diff --git a/plugins/time/package.json b/plugins/time/package.json index 853b4b054b..70fae0761b 100644 --- a/plugins/time/package.json +++ b/plugins/time/package.json @@ -9,7 +9,7 @@ "types/**/*", "tsconfig.json" ], - "author": "Uberflow Contributors", + "author": "Copyright © Hardcore Engineering Inc.", "license": "EPL-2.0", "scripts": { "build": "compile", diff --git a/pods/account/package.json b/pods/account/package.json index 3d6cd452c6..719afe5104 100644 --- a/pods/account/package.json +++ b/pods/account/package.json @@ -62,6 +62,8 @@ "@koa/cors": "^5.0.0", "@hcengineering/server-token": "^0.6.11", "@hcengineering/server-core": "^0.6.1", - "@hcengineering/model-all": "^0.6.0" + "@hcengineering/model-all": "^0.6.0", + "@hcengineering/analytics": "^0.6.0", + "@hcengineering/analytics-service": "^0.6.0" } } diff --git a/pods/account/src/__start.ts b/pods/account/src/__start.ts index f891ce1c73..113f03a061 100644 --- a/pods/account/src/__start.ts +++ b/pods/account/src/__start.ts @@ -1,30 +1,48 @@ // -// Copyright © 2022 Hardcore Engineering Inc. +// Copyright © 2023 Hardcore Engineering Inc. // -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - +import { Analytics } from '@hcengineering/analytics' +import { MeasureMetricsContext, metricsToString, newMetrics, type Tx } from '@hcengineering/core' import { loadBrandingMap } from '@hcengineering/server-core' -import { serveAccount } from '@hcengineering/account-service' -import { MeasureMetricsContext, newMetrics, type Tx } from '@hcengineering/core' +import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service' import builder, { getModelVersion, migrateOperations } from '@hcengineering/model-all' +import { writeFile } from 'fs/promises' +import { join } from 'path' +import { serveAccount } from '@hcengineering/account-service' const enabled = (process.env.MODEL_ENABLED ?? '*').split(',').map((it) => it.trim()) const disabled = (process.env.MODEL_DISABLED ?? '').split(',').map((it) => it.trim()) const txes = JSON.parse(JSON.stringify(builder(enabled, disabled).getTxes())) as Tx[] -const metricsContext = new MeasureMetricsContext('account', {}, {}, newMetrics()) +configureAnalytics(process.env.SENTRY_DSN, {}) +Analytics.setTag('application', 'transactor') + +const metricsContext = new MeasureMetricsContext( + 'account', + {}, + {}, + newMetrics(), + new SplitLogger('account', { + root: join(process.cwd(), 'logs'), + enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true' + }) +) + +let oldMetricsValue = '' + +const intTimer = setInterval(() => { + const val = metricsToString(metricsContext.metrics, 'Account', 140) + if (val !== oldMetricsValue) { + oldMetricsValue = val + void writeFile('metrics.txt', val).catch((err) => { + console.error(err) + }) + } +}, 30000) const brandingPath = process.env.BRANDING_PATH -serveAccount(metricsContext, getModelVersion(), txes, migrateOperations, '', loadBrandingMap(brandingPath)) +serveAccount(metricsContext, getModelVersion(), txes, migrateOperations, '', loadBrandingMap(brandingPath), () => { + clearInterval(intTimer) +}) diff --git a/pods/backup/package.json b/pods/backup/package.json index cc73be451d..86c36c2c6f 100644 --- a/pods/backup/package.json +++ b/pods/backup/package.json @@ -54,6 +54,8 @@ "@hcengineering/client-resources": "^0.6.27", "@hcengineering/core": "^0.6.32", "dotenv": "~16.0.0", - "@hcengineering/backup-service": "^0.6.0" + "@hcengineering/backup-service": "^0.6.0", + "@hcengineering/analytics": "^0.6.0", + "@hcengineering/analytics-service": "^0.6.0" } } diff --git a/pods/backup/src/index.ts b/pods/backup/src/index.ts index a52fe1ad28..8648efad7b 100644 --- a/pods/backup/src/index.ts +++ b/pods/backup/src/index.ts @@ -13,21 +13,58 @@ // limitations under the License. // +import { Analytics } from '@hcengineering/analytics' import { startBackup } from '@hcengineering/backup-service' -import { MeasureMetricsContext } from '@hcengineering/core' +import { MeasureMetricsContext, metricsToString, newMetrics } from '@hcengineering/core' import { DummyDbAdapter, DummyFullTextAdapter, type PipelineFactory } from '@hcengineering/server-core' import { createServerPipeline } from '@hcengineering/server-pipeline' +import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service' +import { writeFile } from 'fs/promises' +import { join } from 'path' -const ctx = new MeasureMetricsContext('backup-service', {}) -startBackup(ctx, (mongoUrl, storageAdapter) => { +const metricsContext = new MeasureMetricsContext( + 'github', + {}, + {}, + newMetrics(), + new SplitLogger('backup-service', { + root: join(process.cwd(), 'logs'), + enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true' + }) +) + +const sentryDSN = process.env.SENTRY_DSN + +configureAnalytics(sentryDSN, {}) +Analytics.setTag('application', 'backup-service') + +let oldMetricsValue = '' + +const intTimer = setInterval(() => { + const val = metricsToString(metricsContext.metrics, 'Backup', 140) + if (val !== oldMetricsValue) { + oldMetricsValue = val + void writeFile('metrics.txt', val).catch((err) => { + console.error(err) + }) + } +}, 30000) + +const onClose = (): void => { + clearInterval(intTimer) + metricsContext.info('Closed') +} + +startBackup(metricsContext, (mongoUrl, storageAdapter) => { const factory: PipelineFactory = createServerPipeline( - ctx, + metricsContext, mongoUrl, { externalStorage: storageAdapter, fullTextUrl: '', indexParallel: 0, indexProcessing: 0, + disableTriggers: true, rekoniUrl: '', usePassedCtx: true }, @@ -47,3 +84,6 @@ startBackup(ctx, (mongoUrl, storageAdapter) => { ) return factory }) + +process.on('SIGINT', onClose) +process.on('SIGTERM', onClose) diff --git a/pods/collaborator/package.json b/pods/collaborator/package.json index 5b1ab2a6e6..1824082a25 100644 --- a/pods/collaborator/package.json +++ b/pods/collaborator/package.json @@ -47,6 +47,8 @@ "dependencies": { "@hcengineering/core": "^0.6.32", "@hcengineering/collaborator": "^0.6.0", - "@hcengineering/platform": "^0.6.11" + "@hcengineering/platform": "^0.6.11", + "@hcengineering/analytics": "^0.6.0", + "@hcengineering/analytics-service": "^0.6.0" } } diff --git a/pods/collaborator/src/__start.ts b/pods/collaborator/src/__start.ts index cb7e70c2e3..56be4a901f 100644 --- a/pods/collaborator/src/__start.ts +++ b/pods/collaborator/src/__start.ts @@ -13,9 +13,39 @@ // limitations under the License. // -import { MeasureMetricsContext, newMetrics } from '@hcengineering/core' +import { Analytics } from '@hcengineering/analytics' +import { MeasureMetricsContext, metricsToString, newMetrics } from '@hcengineering/core' import { startCollaborator } from '@hcengineering/collaborator' +import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service' +import { writeFile } from 'fs/promises' +import { join } from 'path' -const ctx = new MeasureMetricsContext('collaborator', {}, {}, newMetrics()) +configureAnalytics(process.env.SENTRY_DSN, {}) +Analytics.setTag('application', 'collaborator') -void startCollaborator(ctx) +const ctx = new MeasureMetricsContext( + 'collaborator', + {}, + {}, + newMetrics(), + new SplitLogger('collaborator', { + root: join(process.cwd(), 'logs'), + enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true' + }) +) + +let oldMetricsValue = '' + +const intTimer = setInterval(() => { + const val = metricsToString(ctx.metrics, 'Collaborator', 140) + if (val !== oldMetricsValue) { + oldMetricsValue = val + void writeFile('metrics.txt', val).catch((err) => { + console.error(err) + }) + } +}, 30000) + +void startCollaborator(ctx, () => { + clearInterval(intTimer) +}) diff --git a/pods/front/Dockerfile b/pods/front/Dockerfile index ea4a47bff9..acdf2ad0a7 100644 --- a/pods/front/Dockerfile +++ b/pods/front/Dockerfile @@ -12,6 +12,7 @@ ENV LD_PRELOAD=libjemalloc.so.2 ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true COPY bundle/bundle.js ./ +COPY bundle/bundle.js.map ./ COPY dist/ ./dist/ EXPOSE 8080 diff --git a/pods/front/package.json b/pods/front/package.json index 4ff1dcda4a..b4fff43f88 100644 --- a/pods/front/package.json +++ b/pods/front/package.json @@ -14,7 +14,7 @@ "_phase:package": "rushx package", "_phase:docker-build": "rushx docker:build", "_phase:docker-staging": "rushx docker:staging", - "bundle": "mkdir -p bundle && esbuild src/__start.ts --keep-names --define:process.env.MODEL_VERSION=$(node ../../common/scripts/show_version.js) --bundle --minify --platform=node --external:sharp > bundle/bundle.js", + "bundle": "mkdir -p bundle && esbuild src/__start.ts --keep-names --define:process.env.MODEL_VERSION=$(node ../../common/scripts/show_version.js) --bundle --minify --platform=node --external:sharp --outfile=bundle/bundle.js --log-level=error --sourcemap=external", "package": "rm -rf ./dist && cp -r ../../dev/prod/dist . && cp -r ../../dev/prod/public/* ./dist/ && rm ./dist/config.json", "docker:build": "../../common/scripts/docker_build.sh hardcoreeng/front", "docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/front staging", @@ -68,6 +68,8 @@ "@hcengineering/server-token": "^0.6.11", "@hcengineering/attachment": "^0.6.14", "body-parser": "^1.20.2", - "sharp": "~0.32.0" + "sharp": "~0.32.0", + "@hcengineering/analytics": "^0.6.0", + "@hcengineering/analytics-service": "^0.6.0" } } diff --git a/pods/front/src/__start.ts b/pods/front/src/__start.ts index 24c6005920..1a8662e455 100644 --- a/pods/front/src/__start.ts +++ b/pods/front/src/__start.ts @@ -1,17 +1,42 @@ // -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. +// Copyright © 2023 Hardcore Engineering Inc. // -import '@hcengineering/front/src/__start' +import { Analytics } from '@hcengineering/analytics' +import { MeasureMetricsContext, newMetrics } from '@hcengineering/core' +import { startFront } from '@hcengineering/front/src/starter' +import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service' +import { join } from 'path' + +configureAnalytics(process.env.SENTRY_DSN, {}) +Analytics.setTag('application', 'front') + +const metricsContext = new MeasureMetricsContext( + 'front', + {}, + {}, + newMetrics(), + new SplitLogger('front', { + root: join(process.cwd(), 'logs'), + enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true' + }) +) + +startFront(metricsContext, { + GITHUB_APP: process.env.GITHUB_APP ?? '', + GITHUB_CLIENTID: process.env.GITHUB_CLIENTID ?? '', + INTERCOM_APP_ID: process.env.INTERCOM_APP_ID ?? '', + INTERCOM_API_URL: process.env.INTERCOM_API_URL ?? '', + GITHUB_URL: process.env.GITHUB_URL ?? '', + SENTRY_DSN: process.env.SENTRY_DSN ?? '', + LIVEKIT_WS: process.env.LIVEKIT_WS ?? '', + LOVE_ENDPOINT: process.env.LOVE_ENDPOINT ?? '', + SIGN_URL: process.env.SIGN_URL ?? '', + PRINT_URL: process.env.PRINT_URL ?? '', + PUSH_PUBLIC_KEY: process.env.PUSH_PUBLIC_KEY ?? '', + POSTHOG_API_KEY: process.env.POSTHOG_API_KEY, + POSTHOG_HOST: process.env.POSTHOG_HOST, + DESKTOP_UPDATES_URL: process.env.DESKTOP_UPDATES_URL, + DESKTOP_UPDATES_CHANNEL: process.env.DESKTOP_UPDATES_CHANNEL, + ANALYTICS_COLLECTOR_URL: process.env.ANALYTICS_COLLECTOR_URL +}) diff --git a/pods/server/Dockerfile b/pods/server/Dockerfile index c733cd4a02..60431253c7 100644 --- a/pods/server/Dockerfile +++ b/pods/server/Dockerfile @@ -1,8 +1,6 @@ FROM node:20 -ENV NODE_ENV production - -WORKDIR /app +WORKDIR /usr/src/app RUN npm install --ignore-scripts=false --verbose bufferutil utf-8-validate @mongodb-js/zstd snappy --unsafe-perm RUN npm install --ignore-scripts=false --verbose uNetworking/uWebSockets.js#v20.43.0 @@ -13,9 +11,9 @@ ENV LD_PRELOAD=libjemalloc.so.2 ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true COPY bundle/bundle.js ./ +COPY bundle/bundle.js.map ./ RUN mv node_modules/uWebSockets.js/*.node . EXPOSE 8080 - ENV UWS_HTTP_MAX_HEADERS_SIZE 32768 -CMD [ "node", "./bundle.js" ] +CMD node --enable-source-maps --inspect bundle.js diff --git a/pods/server/package.json b/pods/server/package.json index e7a3348ccb..a6c37d705e 100644 --- a/pods/server/package.json +++ b/pods/server/package.json @@ -8,18 +8,20 @@ "template": "@hcengineering/node-package", "license": "EPL-2.0", "scripts": { - "start": "cross-env MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=false SERVER_SECRET=secret REKONI_URL=http://localhost:4004 FRONT_URL=http://localhost:8080 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --nolazy -r ts-node/register src/__start.ts", + "start": "rush bundle --to @hcengineering/server && cross-env NODE_ENV=production ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret node --inspect --enable-source-maps bundle/bundle.js", + "start-u": "rush bundle --to @hcengineering/server && cp ./node_modules/@hcengineering/uws/lib/*.node ./bundle/ && cross-env NODE_ENV=production SERVER_PROVIDER=uweb ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret node --inspect bundle/bundle.js", + "start-flame": "rush bundle --to @hcengineering/server && cross-env NODE_ENV=production ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret clinic flame --dest ./out -- node --nolazy -r ts-node/register --enable-source-maps src/__start.ts", "build": "compile", "_phase:bundle": "rushx bundle", "_phase:docker-build": "rushx docker:build", "_phase:docker-staging": "rushx docker:staging", - "bundle": "mkdir -p bundle && esbuild src/__start.ts --keep-names --bundle --external:*.node --sourcemap=inline --minify --platform=node --external:bufferutil --external:utf-8-validate --define:process.env.MODEL_VERSION=$(node ../../common/scripts/show_version.js) --define:process.env.GIT_REVISION=$(../../common/scripts/git_version.sh) > bundle/bundle.js", + "bundle": "mkdir -p bundle && esbuild src/__start.ts --minify --bundle --keep-names --platform=node --external:*.node --external:bufferutil --external:utf-8-validate --define:process.env.MODEL_VERSION=$(node ../../common/scripts/show_version.js) --define:process.env.GIT_REVISION=$(../../common/scripts/git_version.sh) --outfile=bundle/bundle.js --log-level=error --sourcemap=external", "docker:build": "../../common/scripts/docker_build.sh hardcoreeng/transactor", + "docker:tbuild": "docker build -t hardcoreeng/transactor . --platform=linux/amd64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/transactor", "docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/transactor staging", "docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/transactor", "build:watch": "compile", "format": "format src", - "docker:tbuild": "docker build -t hardcoreeng/transactor . --platform=linux/amd64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/transactor", "test": "jest --passWithNoTests --silent --forceExit", "_phase:build": "compile transpile src", "_phase:test": "jest --passWithNoTests --silent --forceExit", @@ -35,6 +37,7 @@ "eslint-plugin-promise": "^6.1.1", "eslint-plugin-n": "^15.4.0", "eslint": "^8.54.0", + "eslint-config-prettier": "^8.8.0", "@types/ws": "^8.5.11", "ts-node": "^10.8.0", "esbuild": "^0.20.0", @@ -51,10 +54,26 @@ "@hcengineering/platform": "^0.6.11", "@hcengineering/server-core": "^0.6.1", "@hcengineering/server-ws": "^0.6.11", - "@hcengineering/server-token": "^0.6.11", + "@hcengineering/uws": "^0.6.0", "@hcengineering/server": "^0.6.4", "@hcengineering/server-storage": "^0.6.0", "@hcengineering/server-pipeline": "^0.6.0", + "@hcengineering/mongo": "^0.6.1", + "@hcengineering/elastic": "^0.6.0", + "elastic-apm-node": "~3.26.0", + "@hcengineering/server-token": "^0.6.11", + "@hcengineering/middleware": "^0.6.0", + "@hcengineering/minio": "^0.6.0", + "@hcengineering/openai": "^0.6.0", + "@hcengineering/translate": "^0.6.0", + "@hcengineering/server-github": "^0.6.0", + "@hcengineering/server-github-resources": "^0.6.0", + "@hcengineering/github": "^0.6.0", + "@hcengineering/github-assets": "^0.6.0", + "@hcengineering/server-analytics-collector": "^0.6.0", + "@hcengineering/server-analytics-collector-resources": "^0.6.0", + "@hcengineering/analytics": "^0.6.0", + "@hcengineering/analytics-service": "^0.6.0", "@hcengineering/contact": "^0.6.24", "@hcengineering/notification": "^0.6.23", "@hcengineering/server-notification": "^0.6.1" diff --git a/pods/server/src/__start.ts b/pods/server/src/__start.ts index 5a54cb09d6..8ea676b062 100644 --- a/pods/server/src/__start.ts +++ b/pods/server/src/__start.ts @@ -1,40 +1,47 @@ // -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. +// Copyright © 2023 Hardcore Engineering Inc // // Add this to the VERY top of the first file loaded in your app +import { Analytics } from '@hcengineering/analytics' import contactPlugin from '@hcengineering/contact' +import { MeasureMetricsContext, newMetrics } from '@hcengineering/core' import notification from '@hcengineering/notification' import { setMetadata } from '@hcengineering/platform' -import { serverConfigFromEnv } from '@hcengineering/server' +import { getMetricsContext, serverConfigFromEnv } from '@hcengineering/server' import { storageConfigFromEnv } from '@hcengineering/server-storage' import serverCore, { type StorageConfiguration, loadBrandingMap } from '@hcengineering/server-core' import serverNotification from '@hcengineering/server-notification' import serverToken from '@hcengineering/server-token' -import { start } from '.' import { serverFactories } from '@hcengineering/server-ws/src/factories' +import { SplitLogger, configureAnalytics } from '@hcengineering/analytics-service' +import { join } from 'path' +import { start } from '.' const serverFactory = serverFactories[(process.env.SERVER_PROVIDER as string) ?? 'ws'] ?? serverFactories.ws -const config = serverConfigFromEnv() +configureAnalytics(process.env.SENTRY_DSN, {}) +Analytics.setTag('application', 'transactor') +// Force create server metrics context with proper logging +getMetricsContext( + () => + new MeasureMetricsContext( + 'server', + {}, + {}, + newMetrics(), + new SplitLogger('server', { + root: join(process.cwd(), 'logs'), + enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true' + }) + ) +) + +const config = serverConfigFromEnv() const storageConfig: StorageConfiguration = storageConfigFromEnv() -const cursorMaxTime = process.env.SERVER_CURSOR_MAXTIMEMS - const lastNameFirst = process.env.LAST_NAME_FIRST === 'true' -setMetadata(serverCore.metadata.CursorMaxTimeMS, cursorMaxTime) +setMetadata(contactPlugin.metadata.LastNameFirst, lastNameFirst) setMetadata(serverCore.metadata.FrontUrl, config.frontUrl) setMetadata(serverCore.metadata.UploadURL, config.uploadUrl) setMetadata(serverToken.metadata.Secret, config.serverSecret) @@ -42,16 +49,8 @@ setMetadata(serverNotification.metadata.SesUrl, config.sesUrl ?? '') setMetadata(notification.metadata.PushPublicKey, config.pushPublicKey) setMetadata(serverNotification.metadata.PushPrivateKey, config.pushPrivateKey) setMetadata(serverNotification.metadata.PushSubject, config.pushSubject) -setMetadata(contactPlugin.metadata.LastNameFirst, lastNameFirst) setMetadata(serverCore.metadata.ElasticIndexName, config.elasticIndexName) -setMetadata(serverCore.metadata.ElasticIndexVersion, 'v1') -// eslint-disable-next-line @typescript-eslint/no-floating-promises -console.log( - `starting server on ${config.serverPort} git_version: ${process.env.GIT_REVISION ?? ''} model_version: ${ - process.env.MODEL_VERSION ?? '' - }` -) const shutdown = start(config.url, { fullTextUrl: config.elasticUrl, storageConfig, @@ -59,11 +58,11 @@ const shutdown = start(config.url, { port: config.serverPort, serverFactory, indexParallel: 2, - indexProcessing: 50, + indexProcessing: 500, productId: '', brandingMap: loadBrandingMap(config.brandingPath), - enableCompression: config.enableCompression, - accountsUrl: config.accountsUrl + accountsUrl: config.accountsUrl, + enableCompression: config.enableCompression }) const close = (): void => { @@ -74,12 +73,8 @@ const close = (): void => { }) } -process.on('uncaughtException', (e) => { - console.error(e) -}) - process.on('unhandledRejection', (reason, promise) => { - console.error('Unhandled Rejection at:', promise, 'reason:', reason) + console.log('Unhandled Rejection at:', promise, 'reason:', reason) }) process.on('SIGINT', close) diff --git a/qms-tests/docker-compose.yaml b/qms-tests/docker-compose.yaml index 2527c23ed1..1bba36f48c 100644 --- a/qms-tests/docker-compose.yaml +++ b/qms-tests/docker-compose.yaml @@ -137,14 +137,5 @@ services: resources: limits: memory: 1024M - # qms-workers: - # image: hardcoreeng/qms-workers - # restart: always - # links: - # - transactor - # - account - # environment: - # - SECRET=secret - # - ACCOUNTS_URL=http://account:3003 volumes: files: diff --git a/rush.json b/rush.json index be489dcfb6..456ad418d8 100644 --- a/rush.json +++ b/rush.json @@ -466,6 +466,11 @@ "projectFolder": "packages/analytics", "shouldPublish": false }, + { + "packageName": "@hcengineering/analytics-service", + "projectFolder": "packages/analytics-service", + "shouldPublish": false + }, { "packageName": "@hcengineering/storage", "projectFolder": "packages/storage", @@ -1911,6 +1916,166 @@ "packageName": "@hcengineering/model-server-training", "projectFolder": "models/server-training", "shouldPublish": false + }, + { + "packageName": "@hcengineering/desktop", + "projectFolder": "desktop", + "shouldPublish": false + }, + { + "packageName": "desktop", + "projectFolder": "desktop-package", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/diffview", + "projectFolder": "plugins/diffview", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/diffview-assets", + "projectFolder": "plugins/diffview-assets", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/diffview-resources", + "projectFolder": "plugins/diffview-resources", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/github", + "projectFolder": "services/github/github", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/github-assets", + "projectFolder": "services/github/github-assets", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/github-resources", + "projectFolder": "services/github/github-resources", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/model-github", + "projectFolder": "services/github/model-github", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/pod-github", + "projectFolder": "services/github/pod-github", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/server-github", + "projectFolder": "services/github/server-github", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/server-github-resources", + "projectFolder": "services/github/server-github-resources", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/server-github-model", + "projectFolder": "services/github/server-github-model", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/desktop-preferences", + "projectFolder": "plugins/desktop-preferences", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/desktop-preferences-assets", + "projectFolder": "plugins/desktop-preferences-assets", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/desktop-preferences-resources", + "projectFolder": "plugins/desktop-preferences-resources", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/model-desktop-preferences", + "projectFolder": "models/desktop-preferences", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/pod-ses", + "projectFolder": "services/ses/pod-ses", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/pod-telegram", + "projectFolder": "services/telegram/pod-telegram", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/pod-gmail", + "projectFolder": "services/gmail/pod-gmail", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/pod-calendar", + "projectFolder": "services/calendar/pod-calendar", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/qms-doc-import-tool", + "projectFolder": "dev/doc-import-tool", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/pod-love", + "projectFolder": "services/love", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/pod-print", + "projectFolder": "services/print/pod-print", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/pod-sign", + "projectFolder": "services/sign/pod-sign", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/rekoni-service", + "projectFolder": "services/rekoni", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/pod-analytics-collector", + "projectFolder": "services/analytics-collector/pod-analytics-collector", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/analytics-collector", + "projectFolder": "plugins/analytics-collector", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/analytics-collector-assets", + "projectFolder": "plugins/analytics-collector-assets", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/model-analytics-collector", + "projectFolder": "models/analytics-collector", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/server-analytics-collector", + "projectFolder": "server-plugins/analytics-collector", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/server-analytics-collector-resources", + "projectFolder": "server-plugins/analytics-collector-resources", + "shouldPublish": false } ] } diff --git a/server-plugins/analytics-collector-resources/.eslintrc.js b/server-plugins/analytics-collector-resources/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/server-plugins/analytics-collector-resources/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/server-plugins/analytics-collector-resources/.npmignore b/server-plugins/analytics-collector-resources/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/server-plugins/analytics-collector-resources/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/server-plugins/analytics-collector-resources/.prettierignore b/server-plugins/analytics-collector-resources/.prettierignore new file mode 100644 index 0000000000..21c8f7d149 --- /dev/null +++ b/server-plugins/analytics-collector-resources/.prettierignore @@ -0,0 +1 @@ +tsdoc-metadata.json \ No newline at end of file diff --git a/server-plugins/analytics-collector-resources/config/rig.json b/server-plugins/analytics-collector-resources/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/server-plugins/analytics-collector-resources/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/server-plugins/analytics-collector-resources/jest.config.js b/server-plugins/analytics-collector-resources/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/server-plugins/analytics-collector-resources/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/server-plugins/analytics-collector-resources/package.json b/server-plugins/analytics-collector-resources/package.json new file mode 100644 index 0000000000..48f87fac88 --- /dev/null +++ b/server-plugins/analytics-collector-resources/package.json @@ -0,0 +1,48 @@ +{ + "name": "@hcengineering/server-analytics-collector-resources", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@types/jest": "^29.5.5", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "prettier": "^3.1.0", + "ts-jest": "^29.1.1", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/chunter": "^0.6.20", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/core": "^0.6.32", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/server-templates": "^0.6.0", + "@hcengineering/templates": "^0.6.11", + "@hcengineering/analytics-collector": "^0.6.0" + } +} diff --git a/server-plugins/analytics-collector-resources/src/index.ts b/server-plugins/analytics-collector-resources/src/index.ts new file mode 100644 index 0000000000..8c6f050453 --- /dev/null +++ b/server-plugins/analytics-collector-resources/src/index.ts @@ -0,0 +1,19 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export * from './utils' + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export default async () => ({}) diff --git a/server-plugins/analytics-collector-resources/src/utils.ts b/server-plugins/analytics-collector-resources/src/utils.ts new file mode 100644 index 0000000000..6a2e2e24f6 --- /dev/null +++ b/server-plugins/analytics-collector-resources/src/utils.ts @@ -0,0 +1,57 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import chunter, { Channel } from '@hcengineering/chunter' +import core, { AccountRole, Ref, TxOperations } from '@hcengineering/core' +import analyticsCollector, { getAnalyticsChannelName } from '@hcengineering/analytics-collector' +import contact, { Person } from '@hcengineering/contact' +import { translate } from '@hcengineering/platform' + +export async function getOrCreateAnalyticsChannel ( + client: TxOperations, + email: string, + workspace: string, + person?: Person +): Promise | undefined> { + const channel = await client.findOne(chunter.class.Channel, { + [`${analyticsCollector.mixin.AnalyticsChannel}.workspace`]: workspace, + [`${analyticsCollector.mixin.AnalyticsChannel}.email`]: email + }) + + if (channel !== undefined) { + return channel._id + } + + const accounts = await client.findAll(contact.class.PersonAccount, { role: { $ne: AccountRole.Guest } }) + + const _id = await client.createDoc(chunter.class.Channel, core.space.Space, { + name: getAnalyticsChannelName(workspace, email), + topic: await translate(analyticsCollector.string.AnalyticsChannelDescription, { + user: person?.name ?? email, + workspace + }), + description: '', + private: false, + members: accounts.map(({ _id }) => _id), + autoJoin: true, + archived: false + }) + + await client.createMixin(_id, chunter.class.Channel, core.space.Space, analyticsCollector.mixin.AnalyticsChannel, { + workspace, + email + }) + + return _id +} diff --git a/server-plugins/analytics-collector-resources/tsconfig.json b/server-plugins/analytics-collector-resources/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/server-plugins/analytics-collector-resources/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/server-plugins/analytics-collector/.eslintrc.js b/server-plugins/analytics-collector/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/server-plugins/analytics-collector/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/server-plugins/analytics-collector/.npmignore b/server-plugins/analytics-collector/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/server-plugins/analytics-collector/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/server-plugins/analytics-collector/config/rig.json b/server-plugins/analytics-collector/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/server-plugins/analytics-collector/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/server-plugins/analytics-collector/jest.config.js b/server-plugins/analytics-collector/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/server-plugins/analytics-collector/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/server-plugins/analytics-collector/package.json b/server-plugins/analytics-collector/package.json new file mode 100644 index 0000000000..e6384ec673 --- /dev/null +++ b/server-plugins/analytics-collector/package.json @@ -0,0 +1,45 @@ +{ + "name": "@hcengineering/server-analytics-collector", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@types/jest": "^29.5.5", + "@types/node": "~20.11.16", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "prettier": "^3.1.0", + "ts-jest": "^29.1.1", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/analytics-collector": "^0.6.0", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/core": "^0.6.32", + "@hcengineering/server-core": "^0.6.1" + } +} diff --git a/server-plugins/analytics-collector/src/index.ts b/server-plugins/analytics-collector/src/index.ts new file mode 100644 index 0000000000..a892c5d893 --- /dev/null +++ b/server-plugins/analytics-collector/src/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Plugin } from '@hcengineering/platform' + +export const serverAnalyticsCollectorId = 'server-analytics-collector' as Plugin diff --git a/server-plugins/analytics-collector/tsconfig.json b/server-plugins/analytics-collector/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/server-plugins/analytics-collector/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/server/core/src/plugin.ts b/server/core/src/plugin.ts index 64d60c4868..8fbac478f3 100644 --- a/server/core/src/plugin.ts +++ b/server/core/src/plugin.ts @@ -42,7 +42,6 @@ const serverCore = plugin(serverCoreId, { metadata: { FrontUrl: '' as Metadata, UploadURL: '' as Metadata, - CursorMaxTimeMS: '' as Metadata, ElasticIndexName: '' as Metadata, ElasticIndexVersion: '' as Metadata } diff --git a/services/analytics-collector/pod-analytics-collector/.eslintrc.js b/services/analytics-collector/pod-analytics-collector/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/analytics-collector/pod-analytics-collector/.npmignore b/services/analytics-collector/pod-analytics-collector/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/analytics-collector/pod-analytics-collector/Dockerfile b/services/analytics-collector/pod-analytics-collector/Dockerfile new file mode 100644 index 0000000000..05f99cc8df --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/Dockerfile @@ -0,0 +1,8 @@ +FROM node:20-alpine + +WORKDIR /usr/src/app + +COPY bundle/bundle.js ./ + +EXPOSE 4007 +CMD [ "node", "bundle.js" ] diff --git a/services/analytics-collector/pod-analytics-collector/config/rig.json b/services/analytics-collector/pod-analytics-collector/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/analytics-collector/pod-analytics-collector/jest.config.js b/services/analytics-collector/pod-analytics-collector/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/analytics-collector/pod-analytics-collector/package.json b/services/analytics-collector/pod-analytics-collector/package.json new file mode 100644 index 0000000000..b1ea87c06b --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/package.json @@ -0,0 +1,104 @@ +{ + "name": "@hcengineering/pod-analytics-collector", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "_phase:bundle": "rushx bundle", + "_phase:docker-build": "rushx docker:build", + "_phase:docker-staging": "rushx docker:staging", + "bundle": "mkdir -p bundle && esbuild src/index.ts --bundle --platform=node > bundle/bundle.js", + "docker:build": "../../../common/scripts/docker_build.sh hardcoreeng/analytics-collector", + "docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/analytics-collector staging", + "docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/analytics-collector", + "docker:tbuild": "rush bundle --to @hcengineering/pod-analytics-collector && docker build -t hardcoreeng/analytics-collector . --platform=linux/amd64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/analytics-collector", + "run-local": "cross-env APP_ID=$(cat ../../../../uberflow_private/appid) PRIVATE_KEY=\"$(cat ../../../../uberflow_private/private-key.pem)\" CLIENT_ID=$(cat ../../../../uberflow_private/client-id) CLIENT_SECRET=$(cat ../../../../uberflow_private/client-secret) SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000/ COLLABORATOR_URL=http://localhost:3078 COLLABORATOR_API_URL=http://localhost:3078 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ts-node src/index.ts", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@tsconfig/node16": "^1.0.4", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/node": "~20.11.16", + "@types/ws": "^8.5.11", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "esbuild": "^0.20.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "prettier": "^3.1.0", + "ts-node": "^10.8.0", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/account": "^0.6.0", + "@hcengineering/chunter": "^0.6.20", + "@hcengineering/chunter-assets": "^0.6.18", + "@hcengineering/client": "^0.6.18", + "@hcengineering/client-resources": "^0.6.27", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/contact-assets": "^0.6.13", + "@hcengineering/core": "^0.6.32", + "@hcengineering/document": "^0.6.0", + "@hcengineering/document-assets": "^0.6.0", + "@hcengineering/drive": "^0.6.0", + "@hcengineering/drive-assets": "^0.6.0", + "@hcengineering/hr": "^0.6.19", + "@hcengineering/hr-assets": "^0.6.19", + "@hcengineering/lead": "^0.6.0", + "@hcengineering/lead-assets": "^0.6.0", + "@hcengineering/love": "^0.6.0", + "@hcengineering/love-assets": "^0.6.0", + "@hcengineering/notification": "^0.6.23", + "@hcengineering/notification-assets": "^0.6.17", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/preference": "^0.6.13", + "@hcengineering/preference-assets": "^0.6.0", + "@hcengineering/recruit": "^0.6.29", + "@hcengineering/recruit-assets": "^0.6.23", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/server-token": "^0.6.11", + "@hcengineering/setting": "^0.6.17", + "@hcengineering/setting-assets": "^0.6.15", + "@hcengineering/text": "^0.6.5", + "@hcengineering/time": "^0.6.0", + "@hcengineering/time-assets": "^0.6.0", + "@hcengineering/tracker": "^0.6.24", + "@hcengineering/tracker-assets": "^0.6.0", + "@hcengineering/view": "^0.6.13", + "@hcengineering/view-assets": "^0.6.11", + "@hcengineering/workbench": "^0.6.16", + "@hcengineering/workbench-assets": "^0.6.14", + "@hcengineering/analytics-collector": "^0.6.0", + "@hcengineering/analytics-collector-assets": "^0.6.0", + "@hcengineering/server-analytics-collector-resources": "^0.6.0", + "@hcengineering/server-client": "^0.6.0", + "cors": "^2.8.5", + "dotenv": "~16.0.0", + "express": "^4.19.2", + "puppeteer": "^22.6.1", + "ws": "^8.18.0" + } +} diff --git a/services/analytics-collector/pod-analytics-collector/src/account.ts b/services/analytics-collector/pod-analytics-collector/src/account.ts new file mode 100644 index 0000000000..bc4174648f --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/src/account.ts @@ -0,0 +1,21 @@ +import config from './config' +import { WorkspaceLoginInfo } from '@hcengineering/account' + +export async function getWorkspaceInfo (token: string): Promise { + const accountsUrl = config.AccountsUrl + const workspaceInfo = await ( + await fetch(accountsUrl, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + method: 'getWorkspaceInfo', + params: [] + }) + }) + ).json() + + return workspaceInfo.result as WorkspaceLoginInfo +} diff --git a/services/analytics-collector/pod-analytics-collector/src/collector.ts b/services/analytics-collector/pod-analytics-collector/src/collector.ts new file mode 100644 index 0000000000..1c1192f28e --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/src/collector.ts @@ -0,0 +1,173 @@ +import { generateToken, Token } from '@hcengineering/server-token' +import { AnalyticEvent } from '@hcengineering/analytics-collector' +import { AccountRole, getWorkspaceId, Timestamp, toWorkspaceString, WorkspaceId } from '@hcengineering/core' + +import { WorkspaceClient } from './workspaceClient' +import config from './config' +import { getWorkspaceInfo } from './account' + +const clearEventsTimeout = 10 * 60 * 1000 // 10 hour +const eventsTimeToLive = 60 * 60 * 1000 // 1 hour +const closeWorkspaceTimeout = 10 * 60 * 1000 // 10 minutes + +export class Collector { + private readonly workspaces: Map = new Map() + private readonly closeWorkspaceTimeouts: Map = new Map() + private readonly createdWorkspaces: Set = new Set() + + supportClient: WorkspaceClient | undefined = undefined + eventsByEmail = new Map() + + periodicTimer: NodeJS.Timeout + + constructor () { + this.periodicTimer = setInterval(() => { + void this.clearEvents() + }, clearEventsTimeout) + } + + async clearEvents (): Promise { + const now = Date.now() + + for (const [key, events] of this.eventsByEmail.entries()) { + const firstValidIndex = events.findIndex((event) => now - event.timestamp < eventsTimeToLive) + + if (firstValidIndex === -1) { + this.eventsByEmail.delete(key) + } else { + this.eventsByEmail.set(key, events.slice(firstValidIndex)) + } + } + } + + async closeWorkspaceClient (workspaceId: WorkspaceId): Promise { + const workspace = toWorkspaceString(workspaceId) + const timeoutId = this.closeWorkspaceTimeouts.get(workspace) + + if (timeoutId !== undefined) { + clearTimeout(timeoutId) + this.closeWorkspaceTimeouts.delete(workspace) + } + + const client = this.workspaces.get(workspace) + + if (client !== undefined) { + await client.close() + this.workspaces.delete(workspace) + } + } + + async getWorkspaceClient (workspaceId: WorkspaceId): Promise { + const workspace = toWorkspaceString(workspaceId) + const wsClient = this.workspaces.get(workspace) ?? new WorkspaceClient(workspaceId) + + if (!this.workspaces.has(workspace)) { + this.workspaces.set(workspace, wsClient) + } + + const timeoutId = this.closeWorkspaceTimeouts.get(workspace) + + if (timeoutId !== undefined) { + clearTimeout(timeoutId) + } + + const newTimeoutId = setTimeout(() => { + void this.closeWorkspaceClient(workspaceId) + }, closeWorkspaceTimeout) + + this.closeWorkspaceTimeouts.set(workspace, newTimeoutId) + + return wsClient + } + + collect (events: AnalyticEvent[], token: Token): void { + if (toWorkspaceString(token.workspace) === config.SupportWorkspace) { + return + } + + const existingEvents = this.eventsByEmail.get(token.email) ?? [] + + this.eventsByEmail.set(token.email, existingEvents.concat(events)) + void this.pushEvents(events, token) + } + + getSupportWorkspaceClient (): WorkspaceClient { + if (this.supportClient === undefined) { + this.supportClient = new WorkspaceClient(getWorkspaceId(config.SupportWorkspace)) + } + + return this.supportClient + } + + async isWorkspaceCreated (token: Token): Promise { + const ws = toWorkspaceString(token.workspace) + + if (this.createdWorkspaces.has(ws)) { + return true + } + + console.info('isWorkspaceCreated', token.email, token.workspace.name) + const info = await getWorkspaceInfo(generateToken(token.email, token.workspace, token.extra)) + + console.log('workspace info', info?.workspace, info?.email, info?.endpoint) + + if (info === undefined) { + return false + } + if (info?.creating === true) { + return false + } + + this.createdWorkspaces.add(ws) + return true + } + + async pushEvents (events: AnalyticEvent[], token: Token): Promise { + const isCreated = await this.isWorkspaceCreated(token) + + if (!isCreated) { + return + } + + const fromWsClient = await this.getWorkspaceClient(token.workspace) + const account = await fromWsClient.getAccount(token.email) + + if (account === undefined) { + console.error('Cannnot found account', { email: token.email, workspace: toWorkspaceString(token.workspace) }) + return + } + + if (account.role !== AccountRole.Owner) { + return + } + + const person = await fromWsClient.getPerson(account) + const client = this.getSupportWorkspaceClient() + + await client.pushEvents(events, token.email, token.workspace, person) + } + + getEvents (start?: Timestamp, end?: Timestamp): AnalyticEvent[] { + const events = Array.from(this.eventsByEmail.values()) + .flat() + .sort((e1, e2) => e1.timestamp - e2.timestamp) + + if (start === undefined && end === undefined) { + return events + } + + if (start === undefined && end !== undefined) { + return events.filter((e) => e.timestamp <= end) + } + + if (end === undefined && start !== undefined) { + return events.filter((e) => e.timestamp >= start) + } + + return events.filter((e) => e.timestamp >= (start ?? 0) && e.timestamp <= (end ?? 0)) + } + + async close (): Promise { + clearInterval(this.periodicTimer) + } +} diff --git a/services/analytics-collector/pod-analytics-collector/src/config.ts b/services/analytics-collector/pod-analytics-collector/src/config.ts new file mode 100644 index 0000000000..b9704aa3c8 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/src/config.ts @@ -0,0 +1,46 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export interface Config { + Port: number + DbURL: string + Secret: string + ServiceID: string + SupportWorkspace: string + AccountsUrl: string +} + +const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined) + +const config: Config = (() => { + const params: Partial = { + Port: parseNumber(process.env.PORT) ?? 4007, + DbURL: process.env.MONGO_URL, + Secret: process.env.SECRET, + ServiceID: process.env.SERVICE_ID ?? 'analytics-collector-service', + SupportWorkspace: process.env.SUPPORT_WORKSPACE, + AccountsUrl: process.env.ACCOUNTS_URL + } + + const missingEnv = (Object.keys(params) as Array).filter((key) => params[key] === undefined) + + if (missingEnv.length > 0) { + throw Error(`Missing config for attributes: ${missingEnv.join(', ')}`) + } + + return params as Config +})() + +export default config diff --git a/services/analytics-collector/pod-analytics-collector/src/error.ts b/services/analytics-collector/pod-analytics-collector/src/error.ts new file mode 100644 index 0000000000..74f470ab06 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/src/error.ts @@ -0,0 +1,39 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +const generateMessage = (code: number): string => { + if (code === 401) { + return 'Unauthorized' + } + + if (code === 404) { + return 'Not Found' + } + + if (code === 400) { + return 'Bad Request' + } + + return 'Error' +} + +export class ApiError extends Error { + readonly code: number + + constructor (code: number, message?: string) { + super(message ?? generateMessage(code)) + this.code = code + } +} diff --git a/services/analytics-collector/pod-analytics-collector/src/format.ts b/services/analytics-collector/pod-analytics-collector/src/format.ts new file mode 100644 index 0000000000..887e77b11c --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/src/format.ts @@ -0,0 +1,400 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import analyticsCollector, { AnalyticEvent, AnalyticEventType } from '@hcengineering/analytics-collector' +import chunter, { chunterId } from '@hcengineering/chunter' +import contact, { contactId } from '@hcengineering/contact' +import document, { documentId } from '@hcengineering/document' +import drive, { driveId } from '@hcengineering/drive' +import love, { loveId } from '@hcengineering/love' +import notification, { notificationId } from '@hcengineering/notification' +import recruit, { recruitId } from '@hcengineering/recruit' +import time, { timeId } from '@hcengineering/time' +import tracker, { trackerId } from '@hcengineering/tracker' +import { Class, Doc, Hierarchy, Markup, Ref } from '@hcengineering/core' +import { MarkupNode, MarkupNodeType, MarkupMark, MarkupMarkType } from '@hcengineering/text' +import { translate } from '@hcengineering/platform' + +export async function eventToMarkup (event: AnalyticEvent, hierarchy: Hierarchy): Promise { + switch (event.event) { + case AnalyticEventType.CustomEvent: + return formatCustomEvent(event) + case AnalyticEventType.Error: + return await formatErrorEvent(event) + case AnalyticEventType.Navigation: + return await formatNavigationEvent(event, hierarchy) + case AnalyticEventType.SetTag: + return await formatSetTagEvent(event) + case AnalyticEventType.SetUser: + return undefined + } +} + +function toMarkup (items: MarkupNode[]): Markup { + return JSON.stringify({ + type: 'doc', + content: [ + { + type: 'paragraph', + content: items + } + ] + }) +} + +function toText (text: string, display: 'normal' | 'bold' | 'code' = 'normal'): MarkupNode { + const marks: MarkupMark[] = [] + if (display === 'bold') { + marks.push({ type: MarkupMarkType.bold, attrs: {} }) + } + + if (display === 'code') { + marks.push({ type: MarkupMarkType.code, attrs: {} }) + } + + return { type: MarkupNodeType.text, text, marks } +} + +function formatCustomEvent (event: AnalyticEvent): string | undefined { + const text = event.params.event as string | undefined + + if (text === undefined || text === '') return + + return toMarkup([toText(text)]) +} + +async function formatErrorEvent (event: AnalyticEvent): Promise { + const error = event.params.error + + if (error === undefined || (typeof error === 'object' && Object.keys(error).length === 0)) { + return + } + + const text = await translate(analyticsCollector.string.Error, {}) + + return toMarkup([toText(text), toText(error.message ?? JSON.stringify(error), 'code')]) +} + +async function formatSetTagEvent (event: AnalyticEvent): Promise { + const key = event.params.key + + if (key === undefined || key === '' || key === 'workspace') return + + const value = event.params.value + const setLabel = await translate(analyticsCollector.string.Set, {}) + const toLabel = await translate(analyticsCollector.string.To, {}) + + return toMarkup([toText(setLabel), toText(key, 'bold'), toText(toLabel), toText(value, 'bold')]) +} + +async function formatNavigationEvent (event: AnalyticEvent, hierarchy: Hierarchy): Promise { + const path = event.params.path as string | undefined + + if (path === undefined || path === '') return + const location = parseLocation(new URL('http://localhost:8080' + path)) + + if (location.path.length < 3) { + return undefined + } + + return toMarkup(await formatPath(location, hierarchy)) +} + +async function formatPath (location: Location, hierarchy: Hierarchy): Promise { + const { path } = location + + const app = path[2] + + if (app === chunterId) { + const app = await translate(chunter.string.Chat, {}) + return await formatLocWithThreads(location, hierarchy, app) + } + + if (app === notificationId) { + const app = await translate(notification.string.Inbox, {}) + return await formatLocWithThreads(location, hierarchy, app) + } + + if (app === timeId) { + return await formatTimeLoc(location, hierarchy) + } + + if (app === loveId) { + const app = await translate(love.string.Office, {}) + return await navigateToApp(app) + } + + if (app === contactId) { + return await formatContactsLoc(location) + } + + if (app === recruitId) { + return await formatRecruitLoc(location, hierarchy) + } + + if (app === trackerId) { + return await formatTrackerLoc(location, hierarchy) + } + + if (app === documentId) { + return await formatDocumentLoc(location, hierarchy) + } + + if (app === 'team') { + const app = await translate(time.string.Team, {}) + return await navigateToApp(app) + } + + if (app === driveId) { + return await formatDriveLoc(location) + } + + return await formatDefaultLoc(location) +} + +async function navigateToApp (app: string): Promise { + const workbench = await translate(analyticsCollector.string.Workbench, {}) + + return [toText(workbench, 'bold'), toText(app)] +} + +async function navigateToSpecial (app: string, special: string, id?: string): Promise { + const openSpecial = await translate(analyticsCollector.string.OpenSpecial, { special }) + + return [ + toText(app, 'bold'), + toText(': ', 'bold'), + toText(openSpecial), + ...(id !== undefined ? [toText(' '), toText(id, 'code')] : []) + ] +} + +async function formatLocWithThreads (location: Location, hierarchy: Hierarchy, app: string): Promise { + const { path } = location + + if (path.length < 4) { + return await navigateToApp(app) + } + + const [_id, _class] = decodeURIComponent(path[3]).split('|') + + if (_id === undefined || _class === undefined) { + return await navigateToSpecial(app, path[3]) + } + + const label = await translate(hierarchy.getClass(_class as Ref>).label, {}) + const thread = await translate(chunter.string.Thread, {}) + + const openSpecial = await translate(analyticsCollector.string.OpenSpecial, { special: thread }) + + return [ + ...(await navigateToSpecial(app, label, _id)), + ...(path[4] !== undefined ? [toText(', '), toText(openSpecial), toText(path[4], 'code')] : []) + ] +} + +async function formatTimeLoc (location: Location, hierarchy: Hierarchy): Promise { + const { fragment } = location + const app = await translate(time.string.Planner, {}) + + if (fragment === undefined || fragment === '') { + return await navigateToApp(app) + } + + const [, _id, _class] = decodeURIComponent(fragment).split('|') + + if (_id === undefined || _class === undefined) { + return await navigateToApp(app) + } + + const label = await translate(hierarchy.getClass(_class as Ref>).label, {}) + + return await navigateToSpecial(app, label, _id) +} + +async function formatRecruitLoc (location: Location, hierarchy: Hierarchy): Promise { + const { path } = location + const app = await translate(recruit.string.RecruitApplication, {}) + + if (path.length < 4) { + return await navigateToApp(app) + } + + const applicationClass = hierarchy.getClass(recruit.class.Applicant) + const vacancyClass = hierarchy.getClass(recruit.class.Vacancy) + const reviewClass = hierarchy.getClass(recruit.class.Review) + const talentClass = hierarchy.getClass(recruit.mixin.Candidate) + const vacancyListClass = hierarchy.getClass(recruit.mixin.VacancyList) + + const [prefix] = path[3].split('-') + let label: string | undefined + + if (prefix === applicationClass.shortLabel) { + label = await translate(applicationClass.label, {}) + } + + if (prefix === vacancyClass.shortLabel) { + label = await translate(vacancyClass.label, {}) + } + + if (prefix === reviewClass.shortLabel) { + label = await translate(reviewClass.label, {}) + } + + if (prefix === talentClass.shortLabel) { + label = await translate(talentClass.label, {}) + } + + if (prefix === vacancyListClass.shortLabel) { + label = await translate(vacancyListClass.label, {}) + } + + if (label !== undefined) { + return await navigateToSpecial(app, label, path[3]) + } + + return await navigateToSpecial(app, path[3]) +} + +async function formatTrackerLoc (location: Location, hierarchy: Hierarchy): Promise { + const { path, fragment } = location + const app = await translate(tracker.string.TrackerApplication, {}) + + if (path.length < 4) { + return await navigateToApp(app) + } + + if (path.length === 4) { + const issue = await translate(tracker.string.Issue, {}) + return await navigateToSpecial(app, issue, path[3]) + } + + const [, _id, _class] = fragment !== undefined ? decodeURIComponent(fragment).split('|') : [] + + if (_id === undefined || _class === undefined) { + const toSpecialLabel = await navigateToSpecial(app, path[4]) + const inProject = await translate(analyticsCollector.string.InProject, {}) + return [...toSpecialLabel, toText(inProject), toText(path[3], 'code')] + } + + const clazz = hierarchy.getClass(_class as Ref>) + const label = await translate(clazz.label, {}) + const openSpecial = await translate(analyticsCollector.string.OpenSpecial, { special: label }) + const inProject = await translate(analyticsCollector.string.InProject, {}) + + return [ + toText(app, 'bold'), + toText(': ', 'bold'), + toText(openSpecial), + toText(_id, 'code'), + toText(inProject), + toText(path[3], 'code') + ] +} + +async function formatDriveLoc (location: Location): Promise { + const { path } = location + const app = await translate(drive.string.Drive, {}) + + if (path.length < 4) { + return await navigateToApp(app) + } + + if (path.length === 4) { + return await navigateToSpecial(app, path[3]) + } + + const label = await translate(drive.string.Folder, {}) + + return await navigateToSpecial(app, label, path[4]) +} + +async function formatContactsLoc (location: Location): Promise { + const { path } = location + const app = await translate(contact.string.Contacts, {}) + + if (path.length < 4) { + return await navigateToApp(app) + } + + const spaces = ['employees', 'contacts', 'persons', 'companies'] + + if (spaces.includes(path[3])) { + return await navigateToSpecial(app, path[3]) + } + + return [ + toText(app, 'bold'), + toText(': ', 'bold'), + toText(await translate(analyticsCollector.string.Open, {})), + toText(path[3], 'code') + ] +} + +async function formatDocumentLoc (location: Location, hierarchy: Hierarchy): Promise { + const { path } = location + const app = await translate(document.string.Documents, {}) + + if (path.length < 4) { + return await navigateToApp(app) + } + + const clazz = hierarchy.getClass(document.class.Document) + const label = await translate(clazz.label, {}) + return await navigateToSpecial(app, label, path[3]) +} + +async function formatDefaultLoc (location: Location, app?: string): Promise { + const { path } = location + if (path.length < 4) { + return await navigateToApp(app ?? path[2]) + } + + return await navigateToSpecial(app ?? path[2], path[3]) +} + +export interface Location { + path: string[] + fragment?: string +} + +export function parseLocation (location: URL): Location { + return { + path: parsePath(location.pathname), + fragment: parseHash(location.hash) + } +} + +function parsePath (path: string): string[] { + const split = path.split('/').map((ps) => decodeURIComponent(ps)) + if (split.length >= 1) { + if (split[0] === '') { + split.splice(0, 1) + } + } + if (split.length >= 1) { + if (split[split.length - 1] === '') { + split.splice(split.length - 1, 1) + } + } + return split +} + +function parseHash (hash: string): string { + if (hash.startsWith('#')) { + return decodeURIComponent(hash.substring(1)) + } + return decodeURIComponent(hash) +} diff --git a/services/analytics-collector/pod-analytics-collector/src/index.ts b/services/analytics-collector/pod-analytics-collector/src/index.ts new file mode 100644 index 0000000000..7826330a41 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/src/index.ts @@ -0,0 +1,20 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { config } from 'dotenv' +import { main } from './main' + +config() +void main() diff --git a/services/analytics-collector/pod-analytics-collector/src/loaders.ts b/services/analytics-collector/pod-analytics-collector/src/loaders.ts new file mode 100644 index 0000000000..527bd6e1e8 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/src/loaders.ts @@ -0,0 +1,60 @@ +import { analyticsCollectorId } from '@hcengineering/analytics-collector' +import { chunterId } from '@hcengineering/chunter' +import { contactId } from '@hcengineering/contact' +import { coreId } from '@hcengineering/core' +import { documentId } from '@hcengineering/document' +import { driveId } from '@hcengineering/drive' +import { hrId } from '@hcengineering/hr' +import { leadId } from '@hcengineering/lead' +import { loveId } from '@hcengineering/love' +import { notificationId } from '@hcengineering/notification' +import { preferenceId } from '@hcengineering/preference' +import { recruitId } from '@hcengineering/recruit' +import { settingId } from '@hcengineering/setting' +import { timeId } from '@hcengineering/time' +import { trackerId } from '@hcengineering/tracker' +import { viewId } from '@hcengineering/view' +import { workbenchId } from '@hcengineering/workbench' + +import analyticsCollectorEn from '@hcengineering/analytics-collector-assets/lang/en.json' +import chunterEn from '@hcengineering/chunter-assets/lang/en.json' +import contactEn from '@hcengineering/contact-assets/lang/en.json' +import coreEng from '@hcengineering/core/lang/en.json' +import documentEn from '@hcengineering/document-assets/lang/en.json' +import driveEn from '@hcengineering/drive-assets/lang/en.json' +import hrEn from '@hcengineering/hr-assets/lang/en.json' +import leadEn from '@hcengineering/lead-assets/lang/en.json' +import loveEn from '@hcengineering/love-assets/lang/en.json' +import notificationEn from '@hcengineering/notification-assets/lang/en.json' +import platformEng from '@hcengineering/platform/lang/en.json' +import preferenceEn from '@hcengineering/preference-assets/lang/en.json' +import recruitEn from '@hcengineering/recruit-assets/lang/en.json' +import settingEn from '@hcengineering/setting-assets/lang/en.json' +import timeEn from '@hcengineering/time-assets/lang/en.json' +import trackerEn from '@hcengineering/tracker-assets/lang/en.json' +import viewEn from '@hcengineering/view-assets/lang/en.json' +import workbenchEn from '@hcengineering/workbench-assets/lang/en.json' + +import { addStringsLoader, platformId } from '@hcengineering/platform' + +export function registerLoaders (): void { + addStringsLoader(coreId, async (lang: string) => coreEng) + addStringsLoader(platformId, async (lang: string) => platformEng) + + addStringsLoader(analyticsCollectorId, async (lang: string) => analyticsCollectorEn) + addStringsLoader(chunterId, async (lang: string) => chunterEn) + addStringsLoader(contactId, async (lang: string) => contactEn) + addStringsLoader(documentId, async (lang: string) => documentEn) + addStringsLoader(driveId, async (lang: string) => driveEn) + addStringsLoader(hrId, async (lang: string) => hrEn) + addStringsLoader(leadId, async (lang: string) => leadEn) + addStringsLoader(notificationId, async (lang: string) => notificationEn) + addStringsLoader(preferenceId, async (lang: string) => preferenceEn) + addStringsLoader(recruitId, async (lang: string) => recruitEn) + addStringsLoader(settingId, async (lang: string) => settingEn) + addStringsLoader(timeId, async (lang: string) => timeEn) + addStringsLoader(trackerId, async (lang: string) => trackerEn) + addStringsLoader(viewId, async (lang: string) => viewEn) + addStringsLoader(workbenchId, async (lang: string) => workbenchEn) + addStringsLoader(loveId, async (lang: string) => loveEn) +} diff --git a/services/analytics-collector/pod-analytics-collector/src/main.ts b/services/analytics-collector/pod-analytics-collector/src/main.ts new file mode 100644 index 0000000000..41de678498 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/src/main.ts @@ -0,0 +1,55 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { setMetadata } from '@hcengineering/platform' +import serverToken from '@hcengineering/server-token' + +import config from './config' +import { createServer, listen } from './server' +import { Collector } from './collector' +import { registerLoaders } from './loaders' +import serverClient from '@hcengineering/server-client' + +export const main = async (): Promise => { + setMetadata(serverToken.metadata.Secret, config.Secret) + setMetadata(serverClient.metadata.Endpoint, config.AccountsUrl) + setMetadata(serverClient.metadata.UserAgent, config.ServiceID) + + console.log('Analytics service') + console.log(config.AccountsUrl) + console.log(config.DbURL) + console.log(config.SupportWorkspace) + + registerLoaders() + + const collector = new Collector() + + const app = createServer(collector) + const server = listen(app, config.Port) + + const shutdown = (): void => { + void collector.close() + server.close(() => process.exit()) + } + + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + process.on('uncaughtException', (e) => { + console.error(e) + }) + process.on('unhandledRejection', (e) => { + console.error(e) + }) +} diff --git a/services/analytics-collector/pod-analytics-collector/src/platform.ts b/services/analytics-collector/pod-analytics-collector/src/platform.ts new file mode 100644 index 0000000000..a8b94af0d1 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/src/platform.ts @@ -0,0 +1,22 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Client } from '@hcengineering/core' +import { createClient, getTransactorEndpoint } from '@hcengineering/server-client' + +export async function connectPlatform (token: string): Promise { + const endpoint = await getTransactorEndpoint(token) + return await createClient(endpoint, token) +} diff --git a/services/analytics-collector/pod-analytics-collector/src/server.ts b/services/analytics-collector/pod-analytics-collector/src/server.ts new file mode 100644 index 0000000000..71610c1318 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/src/server.ts @@ -0,0 +1,172 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Token, decodeToken } from '@hcengineering/server-token' +import cors from 'cors' +import express, { type Express, type NextFunction, type Request, type Response } from 'express' +import { IncomingHttpHeaders, type Server } from 'http' +import { AnalyticEvent } from '@hcengineering/analytics-collector' + +import { ApiError } from './error' +import { Collector } from './collector' + +const extractCookieToken = (cookie?: string): Token | null => { + if (cookie === undefined || cookie === null) { + return null + } + + const cookies = cookie.split(';') + const tokenCookie = cookies.find((cookie) => cookie.toLocaleLowerCase().includes('token')) + if (tokenCookie === undefined) { + return null + } + + const encodedToken = tokenCookie.split('=')[1] + if (encodedToken === undefined) { + return null + } + + return decodeToken(encodedToken) +} + +const extractAuthorizationToken = (authorization?: string): Token | null => { + if (authorization === undefined || authorization === null) { + return null + } + const encodedToken = authorization.split(' ')[1] + + if (encodedToken === undefined) { + return null + } + + return decodeToken(encodedToken) +} + +const extractToken = (headers: IncomingHttpHeaders): Token => { + try { + const token = extractCookieToken(headers.cookie) ?? extractAuthorizationToken(headers.authorization) + + if (token === null) { + throw new ApiError(401) + } + + return token + } catch { + throw new ApiError(401) + } +} + +type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise + +const handleRequest = async ( + fn: AsyncRequestHandler, + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const token = extractToken(req.headers) + await fn(req, res, token, next) + } catch (err: unknown) { + next(err) + } +} + +const wrapRequest = (fn: AsyncRequestHandler) => (req: Request, res: Response, next: NextFunction) => { + void handleRequest(fn, req, res, next) +} + +function isContentValid (body: any[]): boolean { + return !body.some((it) => { + if (it == null) { + return true + } + + if (!('event' in it)) { + return true + } + + if (!('params' in it)) { + return true + } + + return !('timestamp' in it) + }) +} + +function handleGetEventsRequest (req: Request, res: Response, collector: Collector): void { + const qStart = req.query.start as string + const qEnd = req.query.end as string + + const start = qStart != null ? parseInt(qStart) : undefined + const end = qEnd != null ? parseInt(qEnd) : undefined + + res.status(200) + res.contentType('application/json') + res.send(collector.getEvents(start, end)) +} + +export function createServer (collector: Collector): Express { + const app = express() + app.use(cors()) + app.use(express.json()) + + app.post( + '/collect', + wrapRequest(async (req, res, token) => { + if (req.body == null || !Array.isArray(req.body)) { + throw new ApiError(400) + } + + if (!isContentValid(req.body)) { + throw new ApiError(400) + } + + const events: AnalyticEvent[] = req.body + + collector.collect(events, token) + + res.status(200) + res.json({}) + }) + ) + + app.get( + '/events', + wrapRequest(async (req, res) => { + handleGetEventsRequest(req, res, collector) + }) + ) + + app.use((err: any, _req: any, res: any, _next: any) => { + console.log(err) + if (err instanceof ApiError) { + res.status(err.code).send({ code: err.code, message: err.message }) + return + } + + res.status(500).send(err.message?.length > 0 ? { message: err.message } : err) + }) + + return app +} + +export function listen (e: Express, port: number, host?: string): Server { + const cb = (): void => { + console.log(`Analytics collector service has been started at ${host ?? '*'}:${port}`) + } + + return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb) +} diff --git a/services/analytics-collector/pod-analytics-collector/src/workspaceClient.ts b/services/analytics-collector/pod-analytics-collector/src/workspaceClient.ts new file mode 100644 index 0000000000..4e2df0bf82 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/src/workspaceClient.ts @@ -0,0 +1,131 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { + Client, + Ref, + systemAccountEmail, + toWorkspaceString, + TxOperations, + WorkspaceId +} from '@hcengineering/core' +import { generateToken } from '@hcengineering/server-token' +import chunter, { Channel } from '@hcengineering/chunter' +import contact, { Person, PersonAccount } from '@hcengineering/contact' +import { AnalyticEvent } from '@hcengineering/analytics-collector' +import { getOrCreateAnalyticsChannel } from '@hcengineering/server-analytics-collector-resources' + +import { connectPlatform } from './platform' +import { eventToMarkup } from './format' + +export class WorkspaceClient { + client: Client | undefined + opClient: TxOperations | undefined + + initializePromise: Promise | undefined = undefined + + channelIdByKey = new Map>() + + constructor (readonly workspace: WorkspaceId) { + this.initializePromise = this.initClient().then(() => { + this.initializePromise = undefined + }) + } + + private async initClient (): Promise { + const token = generateToken(systemAccountEmail, this.workspace) + this.client = await connectPlatform(token) + + if (this.client === undefined) { + return + } + + this.opClient = new TxOperations(this.client, core.account.System) + } + + async getAccount (email: string): Promise { + if (this.initializePromise instanceof Promise) { + await this.initializePromise + } + + if (this.opClient === undefined) { + return + } + + return await this.opClient.findOne(contact.class.PersonAccount, { email }) + } + + async getPerson (account: PersonAccount): Promise { + if (this.initializePromise instanceof Promise) { + await this.initializePromise + } + + if (this.opClient === undefined) { + return + } + + return await this.opClient.findOne(contact.class.Person, { _id: account.person }) + } + + async pushEvents (events: AnalyticEvent[], email: string, workspace: WorkspaceId, person?: Person): Promise { + if (this.initializePromise instanceof Promise) { + await this.initializePromise + } + + if (this.opClient === undefined) { + return + } + + const wsString = toWorkspaceString(workspace) + const channelKey = `${email}-${wsString}` + + const channel = + this.channelIdByKey.get(channelKey) ?? (await getOrCreateAnalyticsChannel(this.opClient, email, wsString, person)) + + if (channel === undefined) { + return + } + + this.channelIdByKey.set(channelKey, channel) + + for (const event of events) { + const markup = await eventToMarkup(event, this.opClient.getHierarchy()) + + if (markup === undefined) { + continue + } + + await this.opClient.addCollection( + chunter.class.ChatMessage, + channel, + channel, + chunter.class.Channel, + 'messages', + { message: markup }, + undefined, + event.timestamp + ) + } + } + + async close (): Promise { + if (this.client === undefined) { + return + } + + await this.opClient?.close() + await this.client.close() + } +} diff --git a/services/analytics-collector/pod-analytics-collector/tsconfig.json b/services/analytics-collector/pod-analytics-collector/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/analytics-collector/pod-analytics-collector/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/calendar/pod-calendar/.eslintrc.js b/services/calendar/pod-calendar/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/calendar/pod-calendar/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/calendar/pod-calendar/.npmignore b/services/calendar/pod-calendar/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/calendar/pod-calendar/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/calendar/pod-calendar/Dockerfile b/services/calendar/pod-calendar/Dockerfile new file mode 100644 index 0000000000..bb64763a26 --- /dev/null +++ b/services/calendar/pod-calendar/Dockerfile @@ -0,0 +1,9 @@ + +FROM node:20-alpine + +WORKDIR /usr/src/app + +COPY bundle/bundle.js ./ + +EXPOSE 8095 +CMD [ "node", "bundle.js" ] diff --git a/services/calendar/pod-calendar/config/rig.json b/services/calendar/pod-calendar/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/calendar/pod-calendar/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/calendar/pod-calendar/jest.config.js b/services/calendar/pod-calendar/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/calendar/pod-calendar/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/calendar/pod-calendar/package.json b/services/calendar/pod-calendar/package.json new file mode 100644 index 0000000000..fd9738742b --- /dev/null +++ b/services/calendar/pod-calendar/package.json @@ -0,0 +1,79 @@ +{ + "name": "@hcengineering/pod-calendar", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "_phase:bundle": "rushx bundle", + "_phase:docker-build": "rushx docker:build", + "_phase:docker-staging": "rushx docker:staging", + "bundle": "mkdir -p bundle && esbuild src/index.ts --bundle --platform=node > bundle/bundle.js", + "docker:build": "../../../common/scripts/docker_build.sh hardcoreeng/calendar", + "docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/calendar staging", + "docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/calendar", + "run-local": "cross-env ts-node src/index.ts", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@tsconfig/node16": "^1.0.4", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/node": "~20.11.16", + "@types/node-fetch": "~2.6.2", + "@types/ws": "^8.5.11", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@hcengineering/platform-rig": "^0.6.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@typescript-eslint/parser": "^6.11.0", + "esbuild": "^0.20.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "prettier": "^3.1.0", + "ts-node-dev": "^2.0.0", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/attachment": "^0.6.14", + "@hcengineering/calendar": "^0.6.24", + "@hcengineering/client": "^0.6.18", + "@hcengineering/client-resources": "^0.6.27", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/core": "^0.6.32", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/setting": "^0.6.17", + "@hcengineering/text": "^0.6.5", + "@hcengineering/server-client": "^0.6.0", + "dotenv": "~16.0.0", + "cors": "^2.8.5", + "express": "^4.19.2", + "fast-equals": "^5.0.1", + "file-api": "^0.10.4", + "form-data": "^4.0.0", + "googleapis": "^122.0.0", + "google-auth-library": "^8.0.2", + "jwt-simple": "^0.5.6", + "mongodb": "^6.8.0", + "node-fetch": "^2.6.6", + "ws": "^8.18.0" + } +} diff --git a/services/calendar/pod-calendar/src/base64.ts b/services/calendar/pod-calendar/src/base64.ts new file mode 100644 index 0000000000..7c6165d691 --- /dev/null +++ b/services/calendar/pod-calendar/src/base64.ts @@ -0,0 +1,26 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export function encode64 (str: string): string { + return Buffer.from(str).toString('base64') +} + +export function decode64 (str: string): string { + return Buffer.from(str, 'base64').toString('utf8') +} + +export function arrayBufferToBase64 (buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString('base64') +} diff --git a/services/calendar/pod-calendar/src/calendar.ts b/services/calendar/pod-calendar/src/calendar.ts new file mode 100644 index 0000000000..03daf71223 --- /dev/null +++ b/services/calendar/pod-calendar/src/calendar.ts @@ -0,0 +1,1259 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import calendar, { + Calendar, + Event, + ExternalCalendar, + ReccuringEvent, + ReccuringInstance, + Visibility +} from '@hcengineering/calendar' +import { Contact } from '@hcengineering/contact' +import core, { + Account, + AttachedData, + Client, + Data, + DocumentUpdate, + Ref, + TxOperations, + TxUpdateDoc, + generateId +} from '@hcengineering/core' +import setting from '@hcengineering/setting' +import { htmlToMarkup, markupToHTML } from '@hcengineering/text' +import { deepEqual } from 'fast-equals' +import type { Credentials, OAuth2Client } from 'google-auth-library' +import { calendar_v3, google } from 'googleapis' +import type { Collection, Db } from 'mongodb' +import { encode64 } from './base64' +import { CalendarController } from './calendarController' +import config from './config' +import type { CalendarHistory, EventHistory, EventWatch, ProjectCredentials, State, Token, User, Watch } from './types' +import { encodeReccuring, isToken, parseRecurrenceStrings } from './utils' +import type { WorkspaceClient } from './workspaceClient' +import { RateLimiter } from './rateLimiter' + +const SCOPES = [ + 'https://www.googleapis.com/auth/calendar.calendars.readonly', + 'https://www.googleapis.com/auth/calendar.calendarlist.readonly', + 'https://www.googleapis.com/auth/calendar.events', + 'https://www.googleapis.com/auth/userinfo.email' +] +const DUMMY_RESOURCE = 'Dummy' + +export class CalendarClient { + private readonly oAuth2Client: OAuth2Client + private readonly calendar: calendar_v3.Calendar + private readonly tokens: Collection + private readonly calendarHistories: Collection + private readonly histories: Collection + private readonly client: TxOperations + private me: string | undefined = undefined + private readonly watches: EventWatch[] = [] + private calendarWatch: Watch | undefined = undefined + private refreshTimer: NodeJS.Timeout | undefined = undefined + private readonly activeSync: Record = {} + private readonly rateLimiter = new RateLimiter(1000, 500) + + isClosed: boolean = false + + private constructor ( + credentials: ProjectCredentials, + private readonly user: User, + mongo: Db, + client: Client, + private readonly workspace: WorkspaceClient + ) { + const { client_secret, client_id, redirect_uris } = credentials.web // eslint-disable-line + this.oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]) // eslint-disable-line + this.calendar = google.calendar({ version: 'v3', auth: this.oAuth2Client }) + this.tokens = mongo.collection('tokens') + this.histories = mongo.collection('histories') + this.calendarHistories = mongo.collection('calendarHistories') + this.client = new TxOperations(client, this.user.userId) + } + + static async create ( + credentials: ProjectCredentials, + user: User | Token, + mongo: Db, + client: Client, + workspace: WorkspaceClient + ): Promise { + const calendarClient = new CalendarClient(credentials, user, mongo, client, workspace) + if (isToken(user)) { + await calendarClient.setToken(user) + await calendarClient.refreshToken() + await calendarClient.addClient() + void calendarClient.startSync() + } + return calendarClient + } + + static getAutUrl (redirectURL: string, workspace: string, userId: Ref, token: string): string { + const credentials = JSON.parse(config.Credentials) + const { client_secret, client_id, redirect_uris } = credentials.web // eslint-disable-line + const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]) // eslint-disable-line + const state: State = { + token, + redirectURL, + workspace, + userId, + email: '' + } + const authUrl = oAuth2Client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES, + state: encode64(JSON.stringify(state)) + }) + return authUrl + } + + async authorize (code: string): Promise { + const token = await this.oAuth2Client.getToken(code) + await this.setToken(token.tokens) + const me = await this.getMe() + const providedScopes = token.tokens.scope?.split(' ') ?? [] + for (const scope of SCOPES) { + if (providedScopes.findIndex((p) => p === scope) === -1) { + const integrations = await this.client.findAll(setting.class.Integration, { + createdBy: this.user.userId, + type: calendar.integrationType.Calendar + }) + for (const integration of integrations.filter((p) => p.value === '')) { + await this.client.remove(integration) + } + + const updated = integrations.find((p) => p.disabled && p.value === me) + if (updated !== undefined) { + await this.client.update(updated, { + disabled: true, + error: calendar.string.NotAllPermissions + }) + } else { + await this.client.createDoc(setting.class.Integration, core.space.Workspace, { + type: calendar.integrationType.Calendar, + disabled: true, + error: calendar.string.NotAllPermissions, + value: me + }) + } + throw new Error( + `Not all scopes provided, provided: ${providedScopes.join(', ')} required: ${SCOPES.join(', ')}` + ) + } + } + await this.refreshToken() + await this.addClient() + + const integrations = await this.client.findAll(setting.class.Integration, { + createdBy: this.user.userId, + type: calendar.integrationType.Calendar + }) + + const updated = integrations.find((p) => p.disabled && p.value === me) + + for (const integration of integrations.filter((p) => p.value === '')) { + await this.client.remove(integration) + } + if (updated !== undefined) { + await this.client.update(updated, { + disabled: false, + error: null + }) + } else { + await this.client.createDoc(setting.class.Integration, core.space.Workspace, { + type: calendar.integrationType.Calendar, + disabled: false, + value: me + }) + } + + await this.startSync(me) + void this.syncOurEvents() + + return me + } + + async signout (byError: boolean = false): Promise { + try { + await this.close() + await this.oAuth2Client.revokeCredentials() + } catch {} + await this.tokens.deleteOne({ + userId: this.user.userId, + workspace: this.user.workspace + }) + const integration = await this.client.findOne(setting.class.Integration, { + createdBy: this.user.userId, + type: calendar.integrationType.Calendar, + value: this.user.email + }) + if (integration !== undefined) { + if (byError) { + await this.client.update(integration, { disabled: true }) + } else { + await this.client.remove(integration) + } + } + this.workspace.removeClient(this.user.email) + } + + async startSync (me?: string): Promise { + try { + if (me === undefined) { + me = await this.getMe() + } + await this.syncCalendars(me) + const calendars = this.workspace.getMyCalendars(me) + for (const calendar of calendars) { + if (calendar.externalId !== undefined) { + void this.sync(calendar.externalId, me) + } + } + } catch (err) { + console.log('Start sync error', this.user.workspace, this.user.userId, err) + } + } + + async startSyncCalendar (calendar: ExternalCalendar): Promise { + const me = await this.getMe() + void this.sync(calendar.externalId, me) + } + + async close (): Promise { + if (this.refreshTimer !== undefined) clearTimeout(this.refreshTimer) + for (const watch of this.watches) { + clearTimeout(watch.timer) + try { + if (watch.resourceId !== DUMMY_RESOURCE) { + await this.rateLimiter.take(1) + await this.calendar.channels.stop({ requestBody: { id: watch.channelId, resourceId: watch.resourceId } }) + } + } catch (err) { + console.log('close error', err) + } + } + if (this.calendarWatch !== undefined) { + clearTimeout(this.calendarWatch.timer) + try { + await this.rateLimiter.take(1) + await this.calendar.channels.stop({ + requestBody: { id: this.calendarWatch.channelId, resourceId: this.calendarWatch.resourceId } + }) + } catch (err) { + console.log('close error', err) + } + } + this.isClosed = true + } + + private async getMe (): Promise { + if (this.me !== undefined) { + return this.me + } + + const info = await google.oauth2({ version: 'v2', auth: this.oAuth2Client }).userinfo.get() + this.me = info.data.email ?? '' + return this.me + } + + // #region Token + + private async getCurrentToken (): Promise { + return await this.tokens.findOne({ + userId: this.user.userId, + workspace: this.user.workspace, + email: this.me ?? this.user.email + }) + } + + private async updateCurrentToken (token: Credentials): Promise { + await this.tokens.updateOne( + { + userId: this.user.userId, + workspace: this.user.workspace, + email: this.me ?? this.user.email + }, + { + $set: { + ...token + } + } + ) + } + + private async addClient (): Promise { + try { + const me = await this.getMe() + const controller = CalendarController.getCalendarController() + controller.addClient(me, this) + } catch (err) { + console.log('Add client error', this.user.workspace, this.user.userId, err) + } + } + + private async setToken (token: Credentials): Promise { + try { + this.oAuth2Client.setCredentials(token) + } catch (err: any) { + console.log('Set token error', this.user.workspace, this.user.userId, err) + await this.checkError(err) + throw err + } + } + + private async updateToken (token: Credentials): Promise { + try { + const currentToken = await this.getCurrentToken() + if (currentToken != null) { + await this.updateCurrentToken(token) + } else { + await this.tokens.insertOne({ + userId: this.user.userId, + email: this.me ?? this.user.email, + workspace: this.user.workspace, + token: this.user.token, + ...token + }) + } + } catch (err) { + console.log('update token error', this.user.workspace, this.user.userId, err) + } + } + + private async refreshToken (): Promise { + try { + const res = await this.oAuth2Client.refreshAccessToken() + await this.updateToken(res.credentials) + this.refreshTimer = setTimeout( + () => { + void this.refreshToken() + }, + 30 * 60 * 1000 + ) + } catch (err: any) { + console.log("Couldn't refresh token, error:", err) + if (err?.response?.data?.error === 'invalid_grant' || err.message === 'No refresh token is set.') { + await this.signout(true) + } else { + this.refreshTimer = setTimeout( + () => { + void this.refreshToken() + }, + 15 * 60 * 1000 + ) + } + throw err + } + } + + // #endregion + + // #region Calendars + + private async watchCalendar (): Promise { + try { + const current = this.calendarWatch + if (current !== undefined) { + clearTimeout(current.timer) + await this.rateLimiter.take(1) + await this.calendar.channels.stop({ requestBody: { id: current.channelId, resourceId: current.resourceId } }) + } + const channelId = generateId() + const me = await this.getMe() + const body = { id: channelId, address: config.WATCH_URL, type: 'webhook', token: `user=${me}&mode=calendar` } + await this.rateLimiter.take(1) + const res = await this.calendar.calendarList.watch({ requestBody: body }) + if (res.data.expiration != null && res.data.resourceId !== null) { + const time = Number(res.data.expiration) - new Date().getTime() + // eslint-disable-next-line + const timer = setTimeout(() => void this.watchCalendar(), time) + this.calendarWatch = { + channelId, + resourceId: res.data.resourceId ?? '', + timer + } + } + } catch (err) { + console.log('Calendar watch error', err) + } + } + + async syncCalendars (me: string): Promise { + const history = await this.getCalendarHistory(me) + await this.calendarSync(history?.historyId) + await this.watchCalendar() + } + + private async calendarSync (syncToken?: string, pageToken?: string): Promise { + try { + await this.rateLimiter.take(1) + const res = await this.calendar.calendarList.list({ + syncToken, + pageToken + }) + if (res.status === 410) { + await this.calendarSync() + return + } + const nextPageToken = res.data.nextPageToken + for (const calendar of res.data.items ?? []) { + try { + await this.syncCalendar(calendar) + } catch (err) { + console.log('save calendar error', JSON.stringify(event), err) + } + } + if (nextPageToken != null) { + await this.calendarSync(syncToken, nextPageToken) + } + if (res.data.nextSyncToken != null) { + await this.setCalendarHistoryId(res.data.nextSyncToken) + } + } catch (err: any) { + if (err?.response?.status === 410) { + await this.calendarSync() + return + } + console.log('Calendar sync error', this.user.workspace, this.user.userId, err) + } + } + + private async syncCalendar (val: calendar_v3.Schema$CalendarListEntry): Promise { + if (val.id != null) { + const exists = await this.client.findOne(calendar.class.ExternalCalendar, { + externalId: val.id, + externalUser: this.me ?? '' + }) + if (exists === undefined) { + const data: Data = { + name: val.summary ?? '', + visibility: 'freeBusy', + hidden: false, + externalId: val.id, + externalUser: this.me ?? '', + default: false + } + if (val.primary === true) { + const primaryExists = await this.client.findOne( + calendar.class.ExternalCalendar, + { + createdBy: this.user.userId + }, + { projection: { _id: 1 } } + ) + if (primaryExists === undefined) { + data.default = true + } + } + await this.client.createDoc(calendar.class.ExternalCalendar, calendar.space.Calendar, data) + } else { + const update: DocumentUpdate = {} + if (exists.name !== val.summary) { + update.name = val.summary ?? exists.name + } + if (Object.keys(update).length > 0) { + await this.client.update(exists, update) + } + } + } + } + + private async getCalendarHistory (me: string): Promise { + return await this.calendarHistories.findOne({ + email: me, + userId: this.user.userId, + workspace: this.user.workspace + }) + } + + private async setCalendarHistoryId (historyId: string): Promise { + const me = await this.getMe() + await this.calendarHistories.updateOne( + { + userId: this.user.userId, + workspace: this.user.workspace, + email: me + }, + { + $set: { + historyId + } + }, + { upsert: true } + ) + } + + // #endregion + + // #region Events + + // #region Incoming + + async stopWatch (calendar: ExternalCalendar): Promise { + for (const watch of this.watches) { + if (watch.calendarId === calendar.externalId) { + clearTimeout(watch.timer) + try { + if (watch.resourceId !== DUMMY_RESOURCE) { + await this.rateLimiter.take(1) + await this.calendar.channels.stop({ requestBody: { id: watch.channelId, resourceId: watch.resourceId } }) + } + } catch (err) { + console.log('close error', err) + } + } + } + } + + private async watch (calendarId: string): Promise { + try { + const index = this.watches.findIndex((p) => p.calendarId === calendarId) + if (index !== -1) { + const current = this.watches[index] + if (current !== undefined) { + clearTimeout(current.timer) + if (current.resourceId !== DUMMY_RESOURCE) { + await this.rateLimiter.take(1) + await this.calendar.channels.stop({ + requestBody: { id: current.channelId, resourceId: current.resourceId } + }) + } + } + this.watches.splice(index, 1) + } + const channelId = generateId() + const me = await this.getMe() + const body = { + id: channelId, + address: config.WATCH_URL, + type: 'webhook', + token: `user=${me}&mode=events&calendarId=${calendarId}` + } + await this.rateLimiter.take(1) + const res = await this.calendar.events.watch({ calendarId, requestBody: body }) + if (res.data.expiration != null && res.data.resourceId != null) { + const time = Number(res.data.expiration) - new Date().getTime() + // eslint-disable-next-line + const timer = setTimeout(() => void this.watch(calendarId), time) + this.watches.push({ + calendarId, + channelId, + resourceId: res.data.resourceId ?? '', + timer + }) + } + } catch (err: any) { + if (err?.errors?.[0]?.reason === 'pushNotSupportedForRequestedResource') { + await this.dummyWatch(calendarId) + } else { + console.log('Watch error', err) + await this.checkError(err) + } + } + } + + private async checkError (err: any): Promise { + if (err?.response?.data?.error === 'invalid_grant') { + await this.signout(true) + return true + } + return false + } + + private async dummyWatch (calendarId: string): Promise { + const me = await this.getMe() + const timer = setTimeout( + () => { + void this.sync(calendarId, me) + }, + 6 * 60 * 60 * 1000 + ) + this.watches.push({ + calendarId, + channelId: DUMMY_RESOURCE, + resourceId: DUMMY_RESOURCE, + timer + }) + } + + async sync (calendarId: string, me: string): Promise { + if (this.isClosed) return + if (this.activeSync[calendarId]) return + this.activeSync[calendarId] = true + await this.syncEvents(calendarId, me) + this.activeSync[calendarId] = false + await this.watch(calendarId) + } + + private async getEventHistory (calendarId: string, me: string): Promise { + return await this.histories.findOne({ + calendarId, + email: me, + userId: this.user.userId, + workspace: this.user.workspace + }) + } + + private async setEventHistoryId (calendarId: string, historyId: string): Promise { + const me = await this.getMe() + await this.histories.updateOne( + { + calendarId, + userId: this.user.userId, + workspace: this.user.workspace, + email: me + }, + { + $set: { + historyId + } + }, + { upsert: true } + ) + } + + private async syncEvents (calendarId: string, me: string): Promise { + const history = await this.getEventHistory(calendarId, me) + await this.eventsSync(calendarId, history?.historyId) + } + + private async eventsSync (calendarId: string, syncToken?: string, pageToken?: string): Promise { + try { + await this.rateLimiter.take(1) + const res = await this.calendar.events.list({ + calendarId, + syncToken, + pageToken, + showDeleted: syncToken != null + }) + if (res.status === 410) { + await this.eventsSync(calendarId) + return + } + const nextPageToken = res.data.nextPageToken + for (const event of res.data.items ?? []) { + try { + await this.syncEvent(calendarId, event, res.data.accessRole ?? 'reader') + } catch (err) { + console.log('save event error', JSON.stringify(event), err) + } + } + if (nextPageToken != null) { + await this.eventsSync(calendarId, syncToken, nextPageToken) + } + if (res.data.nextSyncToken != null) { + await this.setEventHistoryId(calendarId, res.data.nextSyncToken) + } + } catch (err: any) { + if (err?.response?.status === 410) { + await this.eventsSync(calendarId) + return + } + await this.checkError(err) + console.log('Event sync error', this.user.workspace, this.user.userId, err) + } + } + + private async syncEvent (calendarId: string, event: calendar_v3.Schema$Event, accessRole: string): Promise { + console.log('SYNC EVENT CALL', calendarId, JSON.stringify(event)) + if (event.id != null) { + const me = await this.getMe() + const calendars = this.workspace.getMyCalendars(me) + const _calendar = + calendars.find((p) => p.externalId === event.organizer?.email) ?? + calendars.find((p) => p.externalId === calendarId) ?? + calendars[0] + console.log('Target space', _calendar) + if (_calendar !== undefined) { + const exists = (await this.client.findOne(calendar.class.Event, { + eventId: event.id, + calendar: _calendar._id + })) as Event | undefined + if (exists === undefined) { + await this.saveExtEvent(event, accessRole, _calendar) + } else { + await this.updateExtEvent(event, exists) + } + } + } + } + + private async updateExtEvent (event: calendar_v3.Schema$Event, current: Event): Promise { + console.log('UPDATE EVENT', current._id, JSON.stringify(event)) + if (event.status === 'cancelled' && current._class !== calendar.class.ReccuringInstance) { + console.log('REMOVE EVENT', current._id) + await this.client.remove(current) + return + } + const data: Partial> = await this.parseUpdateData(event) + if (event.recurringEventId != null) { + const diff = this.getEventDiff( + { + ...data, + recurringEventId: event.recurringEventId as Ref, + originalStartTime: parseDate(event.originalStartTime), + isCancelled: event.status === 'cancelled' + }, + current as ReccuringInstance + ) + if (Object.keys(diff).length > 0) { + console.log('UPDATE REC INSTANCE DIFF', JSON.stringify(diff), JSON.stringify(current)) + await this.client.update(current, diff) + } + } else { + if (event.recurrence != null) { + const parseRule = parseRecurrenceStrings(event.recurrence) + const diff = this.getEventDiff( + { + ...data, + rules: parseRule.rules, + exdate: parseRule.exdate, + rdate: parseRule.rdate + }, + current as ReccuringEvent + ) + if (Object.keys(diff).length > 0) { + console.log('UPDATE REC EVENT DIFF', JSON.stringify(diff), JSON.stringify(current)) + await this.client.update(current, diff) + } + } else { + const diff = this.getEventDiff(data, current) + if (Object.keys(diff).length > 0) { + console.log('UPDATE EVENT DIFF', JSON.stringify(diff), JSON.stringify(current)) + await this.client.update(current, diff) + } + } + } + } + + private async saveExtEvent ( + event: calendar_v3.Schema$Event, + accessRole: string, + _calendar: ExternalCalendar + ): Promise { + const data: AttachedData = await this.parseData(event, accessRole, _calendar._id) + if (event.recurringEventId != null) { + const parseRule = parseRecurrenceStrings(event.recurrence ?? []) + const id = await this.client.addCollection( + calendar.class.ReccuringInstance, + calendar.space.Calendar, + calendar.ids.NoAttached, + calendar.class.Event, + 'events', + { + ...data, + recurringEventId: event.recurringEventId, + originalStartTime: parseDate(event.originalStartTime), + isCancelled: event.status === 'cancelled', + rules: parseRule.rules, + exdate: parseRule.exdate, + rdate: parseRule.rdate, + timeZone: event.start?.timeZone ?? event.end?.timeZone ?? 'Etc/GMT' + } + ) + console.log('SAVE INSTANCE', id, JSON.stringify(event)) + } else if (event.status !== 'cancelled') { + if (event.recurrence != null) { + const parseRule = parseRecurrenceStrings(event.recurrence) + const id = await this.client.addCollection( + calendar.class.ReccuringEvent, + calendar.space.Calendar, + calendar.ids.NoAttached, + calendar.class.Event, + 'events', + { + ...data, + rules: parseRule.rules, + exdate: parseRule.exdate, + rdate: parseRule.rdate, + originalStartTime: data.date, + timeZone: event.start?.timeZone ?? event.end?.timeZone ?? 'Etc/GMT' + } + ) + console.log('SAVE REC EVENT', id, JSON.stringify(event)) + } else { + const id = await this.client.addCollection( + calendar.class.Event, + calendar.space.Calendar, + calendar.ids.NoAttached, + calendar.class.Event, + 'events', + data + ) + console.log('SAVE EVENT', id, JSON.stringify(event)) + } + } + } + + private getEventDiff(data: Partial>, current: T): Partial> { + const res = {} + for (const key in data) { + if (!deepEqual((data as any)[key], (current as any)[key])) { + ;(res as any)[key] = (data as any)[key] + } + } + return res + } + + private async parseUpdateData (event: calendar_v3.Schema$Event): Promise>> { + const res: Partial> = {} + if (event.attendees !== undefined) { + const participants = await this.getParticipants(event) + res.participants = participants[0] + if (participants[1].length > 0) { + res.externalParticipants = participants[1] + } + } + if (event.location != null) { + res.location = event.location + } + if (event.description != null) { + res.description = htmlToMarkup(event.description) + } + if (event.summary != null) { + res.title = event.summary + } + if (event.start != null) { + res.date = parseDate(event.start) + } + if (event.end != null) { + res.dueDate = parseDate(event.end) + } + if (event.visibility != null && event.visibility !== 'default') { + res.visibility = + event.visibility === 'public' + ? 'public' + : (event.extendedProperties?.private?.visibility as Visibility) ?? 'private' + } + + return res + } + + private getAccess ( + event: calendar_v3.Schema$Event, + accessRole: string + ): 'freeBusyReader' | 'reader' | 'writer' | 'owner' { + if (accessRole !== 'owner') { + return accessRole as 'freeBusyReader' | 'reader' | 'writer' + } + if (event.creator?.self === true) { + return 'owner' + } else { + return 'reader' + } + } + + private async parseData ( + event: calendar_v3.Schema$Event, + accessRole: string, + _calendar: Ref + ): Promise> { + const participants = await this.getParticipants(event) + const res: AttachedData = { + date: parseDate(event.start), + dueDate: parseDate(event.end), + allDay: event.start?.date != null, + description: htmlToMarkup(event.description ?? ''), + title: event.summary ?? '', + location: event.location ?? undefined, + participants: participants[0], + eventId: event.id ?? '', + calendar: _calendar, + access: this.getAccess(event, accessRole), + timeZone: event.start?.timeZone ?? event.end?.timeZone ?? 'Etc/GMT' + } + if (participants[1].length > 0) { + res.externalParticipants = participants[1] + } + if (event.visibility != null && event.visibility !== 'default') { + res.visibility = + event.visibility === 'public' + ? 'public' + : (event.extendedProperties?.private?.visibility as Visibility) ?? 'private' + } + return res + } + + private getParticipant (value: string): { + contact?: Ref + extra?: string + } { + const integration = this.workspace.integrations.byEmail.get(value) + if (integration != null) { + return { + contact: integration + } + } + const contact = this.workspace.contacts.byEmail.get(value) + if (contact !== undefined) { + return { + contact + } + } + return { + extra: value + } + } + + private async getParticipants (event: calendar_v3.Schema$Event): Promise<[Ref[], string[]]> { + const contacts = new Set>() + const extra = new Set() + if (event.creator?.email != null) { + const res = this.getParticipant(event.creator.email) + if (res.contact !== undefined) { + contacts.add(res.contact) + } + if (res.extra !== undefined) { + extra.add(res.extra) + } + } + for (const attendee of event.attendees ?? []) { + if (attendee.email != null) { + const res = this.getParticipant(attendee.email) + if (res.contact !== undefined) { + contacts.add(res.contact) + } + if (res.extra !== undefined) { + extra.add(res.extra) + } + } + } + return [Array.from(contacts), Array.from(extra)] + } + + // #endregion + + // #region Outcoming + + private areDatesEqual (first: calendar_v3.Schema$EventDateTime, second: calendar_v3.Schema$EventDateTime): boolean { + if (first.date != null && second.date != null) { + return new Date(first.date).getTime() === new Date(second.date).getTime() + } + if (first.dateTime != null && second.dateTime != null) { + return new Date(first.dateTime).getTime() === new Date(second.dateTime).getTime() + } + return false + } + + private async createRecInstance (calendarId: string, event: ReccuringInstance): Promise { + const body = this.convertBody(event) + const req: calendar_v3.Params$Resource$Events$Instances = { + calendarId, + eventId: event.recurringEventId + } + await this.rateLimiter.take(1) + const instancesResp = await this.calendar.events.instances(req) + const items = instancesResp.data.items + const target = items?.find( + (p) => + p.originalStartTime != null && + body.originalStartTime != null && + this.areDatesEqual(p.originalStartTime, body.originalStartTime) + ) + if (target?.id != null) { + body.id = target.id + await this.rateLimiter.take(1) + await this.calendar.events.update({ + calendarId, + eventId: target.id, + requestBody: body + }) + await this.client.update(event, { eventId: body.id ?? '' }) + } + } + + async createEvent (event: Event): Promise { + try { + const _calendar = this.workspace.calendars.byId.get(event.calendar as Ref) + if (_calendar !== undefined) { + if (event._class === calendar.class.ReccuringInstance) { + await this.createRecInstance(_calendar.externalId, event as ReccuringInstance) + } else { + const body = this.convertBody(event) + await this.rateLimiter.take(1) + await this.calendar.events.insert({ + calendarId: _calendar.externalId, + requestBody: body + }) + } + } + } catch (err: any) { + await this.checkError(err) + // eslint-disable-next-line + throw new Error(`Create event error, ${this.user.workspace}, ${this.user.userId}, ${event._id}, ${err?.message}`) + } + } + + async updateEvent (event: Event, tx: TxUpdateDoc): Promise { + const _calendar = this.workspace.calendars.byId.get(event.calendar as Ref) + const calendarId = _calendar?.externalId + if (calendarId !== undefined) { + try { + await this.rateLimiter.take(1) + const current = await this.calendar.events.get({ calendarId, eventId: event.eventId }) + if (current?.data !== undefined) { + if (current.data.organizer?.self === true) { + const ev = this.applyUpdate(current.data, event) + await this.rateLimiter.take(1) + await this.calendar.events.update({ + calendarId, + eventId: event.eventId, + requestBody: ev + }) + } + } + } catch (err: any) { + if (err.code === 404) { + await this.createEvent(event) + } else { + console.log('Update event error', this.user.workspace, this.user.userId, err) + await this.checkError(err) + } + } + } + } + + async remove (eventId: string, calendarId: string): Promise { + const current = await this.calendar.events.get({ calendarId, eventId }) + if (current?.data !== undefined) { + if (current.data.organizer?.self === true) { + await this.rateLimiter.take(1) + await this.calendar.events.delete({ + eventId, + calendarId + }) + } + } + } + + async removeEvent (event: Event): Promise { + try { + const _calendar = this.workspace.calendars.byId.get(event.calendar as Ref) + if (_calendar !== undefined) { + await this.remove(event.eventId, _calendar.externalId) + } + } catch (err) { + console.log('Remove event error', this.user.workspace, this.user.userId, err) + } + } + + async syncOurEvents (): Promise { + const me = await this.getMe() + const events = await this.client.findAll(calendar.class.Event, { + access: 'owner', + createdBy: this.user.userId, + calendar: { $in: this.workspace.getMyCalendars(me).map((p) => p._id) } + }) + for (const event of events) { + await this.syncMyEvent(event) + } + await this.workspace.updateSyncTime() + } + + async syncMyEvent (event: Event): Promise { + if (event.access === 'owner' || event.access === 'writer') { + try { + const space = this.workspace.calendars.byId.get(event.calendar as Ref) + if (space !== undefined && space.externalUser === this.me) { + if (!(await this.update(event, space))) { + await this.create(event, space) + } + } + } catch (err: any) { + console.log('Sync event error', this.user.workspace, this.user.userId, event._id, err.message) + } + } + } + + private async create (event: Event, space: ExternalCalendar): Promise { + const body = this.convertBody(event) + const calendarId = space?.externalId + if (calendarId !== undefined) { + await this.rateLimiter.take(1) + await this.calendar.events.insert({ + calendarId, + requestBody: body + }) + } + } + + private async update (event: Event, space: ExternalCalendar): Promise { + const calendarId = space?.externalId + if (calendarId !== undefined) { + try { + await this.rateLimiter.take(1) + const current = await this.calendar.events.get({ calendarId, eventId: event.eventId }) + if (current !== undefined) { + const ev = this.applyUpdate(current.data, event) + await this.rateLimiter.take(1) + await this.calendar.events.update({ + calendarId, + eventId: event.eventId, + requestBody: ev + }) + } + return true + } catch (err: any) { + if (err.code === 404) { + return false + } else { + throw err + } + } + } + return false + } + + private convertBody (event: Event): calendar_v3.Schema$Event { + const res: calendar_v3.Schema$Event = { + start: convertDate(event.date, event.allDay, getTimezone(event)), + end: convertDate(event.dueDate, event.allDay, getTimezone(event)), + id: event.eventId, + description: markupToHTML(event.description), + summary: event.title + } + if (event.location != null) { + res.location = event.location + } + if (event.visibility !== undefined) { + res.visibility = event.visibility === 'public' ? 'public' : 'private' + } + if (event.visibility === 'freeBusy') { + res.extendedProperties = { + private: { + visibility: 'freeBusy' + } + } + } + if (event.reminders !== undefined) { + res.reminders = { + useDefault: false, + overrides: event.reminders.map((p) => { + return { method: 'popup', minutes: p / 60 / 1000 } + }) + } + } + const attendees = this.getAttendees(event) + if (attendees.length > 0) { + res.attendees = attendees.map((p) => { + if (p === this.me) { + return { email: p, responseStatus: 'accepted', self: true } + } + return { email: p } + }) + } + if (event._class === calendar.class.ReccuringInstance) { + const instance = event as ReccuringInstance + res.recurringEventId = instance.recurringEventId + res.originalStartTime = convertDate(instance.originalStartTime ?? event.date, event.allDay, instance.timeZone) + res.status = instance.isCancelled === true ? 'cancelled' : undefined + } + if (event._class === calendar.class.ReccuringEvent) { + const rec = event as ReccuringEvent + res.recurrence = encodeReccuring(rec.rules, rec.rdate, rec.exdate) + } + return res + } + + private applyUpdate (event: calendar_v3.Schema$Event, current: Event): calendar_v3.Schema$Event { + if (current.title !== event.summary) { + event.summary = current.title + } + if (current.visibility !== undefined) { + const newVisibility = current.visibility === 'public' ? 'public' : 'private' + if (newVisibility !== event.visibility) { + event.visibility = newVisibility + } + if (current.visibility === 'freeBusy') { + event.extendedProperties = { + ...event.extendedProperties, + private: { + visibility: 'freeBusy' + } + } + } + } + const description = htmlToMarkup(event.description ?? '') + if (current.description !== description) { + event.description = description + } + if (current.location !== event.location) { + event.location = current.location + } + const attendees = this.getAttendees(current) + if (attendees.length > 0 && event.attendees !== undefined) { + for (const attendee of attendees) { + if (event.attendees.findIndex((p) => p.email === attendee) === -1) { + event.attendees.push({ email: attendee }) + } + } + } + event.start = convertDate(current.date, event.start?.date !== undefined, getTimezone(current)) + event.end = convertDate(current.dueDate, event.end?.date !== undefined, getTimezone(current)) + if (current._class === calendar.class.ReccuringEvent) { + const rec = current as ReccuringEvent + event.recurrence = encodeReccuring(rec.rules, rec.rdate, rec.exdate) + } + return event + } + + private getAttendees (event: Event): string[] { + const res = new Set() + for (const participant of event.participants) { + const integrations = this.workspace.integrations.byContact.get(participant) ?? [] + const integration = integrations.find((p) => p === this.me) ?? integrations[0] + if (integration !== undefined && integration !== '') { + res.add(integration) + } else { + const contact = this.workspace.contacts.byId.get(participant) + if (contact !== undefined) { + res.add(contact) + } + } + } + for (const ext of event.externalParticipants ?? []) { + res.add(ext) + } + return Array.from(res) + } + + // #endregion + + // #endregion +} + +function parseDate (date: calendar_v3.Schema$EventDateTime | undefined): number { + if (date?.dateTime != null) { + return new Date(date.dateTime).getTime() + } + if (date?.date != null) { + return new Date(date.date).getTime() + } + return 0 +} + +function convertDate (value: number, allDay: boolean, timeZone: string | undefined): calendar_v3.Schema$EventDateTime { + return allDay + ? { date: new Date(value).toISOString().split('T')[0] } + : { dateTime: new Date(value).toISOString(), timeZone: timeZone ?? 'Etc/GMT' } +} + +function getTimezone (event: Event): string | undefined { + return event.timeZone +} diff --git a/services/calendar/pod-calendar/src/calendarController.ts b/services/calendar/pod-calendar/src/calendarController.ts new file mode 100644 index 0000000000..b13d08fe23 --- /dev/null +++ b/services/calendar/pod-calendar/src/calendarController.ts @@ -0,0 +1,140 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Account, Ref } from '@hcengineering/core' +import { type Db } from 'mongodb' +import { type CalendarClient } from './calendar' +import config from './config' +import { type ProjectCredentials, type Token, type User } from './types' +import { WorkspaceClient } from './workspaceClient' + +export class CalendarController { + private readonly workspaces: Map = new Map() + + private readonly credentials: ProjectCredentials + private readonly clients: Map = new Map() + + protected static _instance: CalendarController + + private constructor (private readonly mongo: Db) { + this.credentials = JSON.parse(config.Credentials) + CalendarController._instance = this + } + + static getCalendarController (mongo?: Db): CalendarController { + if (CalendarController._instance !== undefined) { + return CalendarController._instance + } + if (mongo === undefined) throw new Error('CalendarController not exist') + return new CalendarController(mongo) + } + + async startAll (): Promise { + const tokens = await this.mongo.collection('tokens').find().toArray() + for (const token of tokens) { + try { + await this.createClient(token) + } catch (err) { + console.error(`Couldn't create client for ${token.workspace} ${token.userId}`) + } + } + + for (const client of this.workspaces.values()) { + void client.sync() + } + } + + push (email: string, mode: 'events' | 'calendar', calendarId?: string): void { + const clients = this.clients.get(email) + for (const client of clients ?? []) { + if (mode === 'calendar') { + void client.syncCalendars(email) + } + if (mode === 'events' && calendarId !== undefined) { + void client.sync(calendarId, email) + } + } + } + + addClient (email: string, client: CalendarClient): void { + const clients = this.clients.get(email) + if (clients === undefined) { + this.clients.set(email, [client]) + } else { + clients.push(client) + this.clients.set(email, clients) + } + } + + removeClient (email: string): void { + const clients = this.clients.get(email) + if (clients !== undefined) { + this.clients.set( + email, + clients.filter((p) => !p.isClosed) + ) + } + } + + async getUserId (email: string, workspace: string): Promise> { + const workspaceClient = await this.getWorkspaceClient(workspace) + return await workspaceClient.getUserId(email) + } + + async signout (workspace: string, value: string): Promise { + const workspaceClient = await this.getWorkspaceClient(workspace) + const clients = await workspaceClient.signout(value) + if (clients === 0) { + this.workspaces.delete(workspace) + } + } + + removeWorkspace (workspace: string): void { + this.workspaces.delete(workspace) + } + + async close (): Promise { + for (const workspace of this.workspaces.values()) { + await workspace.close() + } + this.workspaces.clear() + } + + async createClient (user: Token): Promise { + const workspace = await this.getWorkspaceClient(user.workspace) + const newClient = await workspace.createCalendarClient(user) + return newClient + } + + async newClient (user: User, code: string): Promise { + const workspace = await this.getWorkspaceClient(user.workspace) + const newClient = await workspace.newCalendarClient(user, code) + return newClient + } + + private async getWorkspaceClient (workspace: string): Promise { + let res = this.workspaces.get(workspace) + if (res === undefined) { + try { + res = await WorkspaceClient.create(this.credentials, this.mongo, workspace, this) + this.workspaces.set(workspace, res) + } catch (err) { + console.error(`Couldn't create workspace worker for ${workspace}, reason: ${JSON.stringify(err)}`) + throw err + } + } + return res + } +} diff --git a/services/calendar/pod-calendar/src/client.ts b/services/calendar/pod-calendar/src/client.ts new file mode 100644 index 0000000000..fa222927d8 --- /dev/null +++ b/services/calendar/pod-calendar/src/client.ts @@ -0,0 +1,22 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Client } from '@hcengineering/core' +import { createClient, getTransactorEndpoint } from '@hcengineering/server-client' + +export async function getClient (token: string): Promise { + const endpoint = await getTransactorEndpoint(token) + return await createClient(endpoint, token) +} diff --git a/services/calendar/pod-calendar/src/config.ts b/services/calendar/pod-calendar/src/config.ts new file mode 100644 index 0000000000..73327ac3df --- /dev/null +++ b/services/calendar/pod-calendar/src/config.ts @@ -0,0 +1,72 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +interface Config { + Port: number + + MongoURI: string + MongoDB: string + AccountsURL: string + UploadUrl: string + ServiceID: string + Secret: string + Credentials: string + WATCH_URL: string + SystemEmail: string +} + +const envMap: { [key in keyof Config]: string } = { + Port: 'PORT', + + MongoURI: 'MONGO_URI', + MongoDB: 'MONGO_DB', + + AccountsURL: 'ACCOUNTS_URL', + UploadUrl: 'UPLOAD_URL', + ServiceID: 'SERVICE_ID', + Secret: 'SECRET', + Credentials: 'Credentials', + SystemEmail: 'SYSTEM_EMAIL', + WATCH_URL: 'WATCH_URL' +} + +const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined) + +const config: Config = (() => { + const params: Partial = { + Port: parseNumber(process.env[envMap.Port]) ?? 8095, + MongoDB: process.env[envMap.MongoDB] ?? 'calendar-service', + MongoURI: process.env[envMap.MongoURI], + AccountsURL: process.env[envMap.AccountsURL], + UploadUrl: process.env[envMap.UploadUrl], + ServiceID: process.env[envMap.ServiceID] ?? 'calendar-service', + Secret: process.env[envMap.Secret], + SystemEmail: process.env[envMap.SystemEmail] ?? 'anticrm@hc.engineering', + Credentials: process.env[envMap.Credentials], + WATCH_URL: process.env[envMap.WATCH_URL] + } + + const missingEnv = (Object.keys(params) as Array) + .filter((key) => params[key] === undefined) + .map((key) => envMap[key]) + + if (missingEnv.length > 0) { + throw Error(`Missing env variables: ${missingEnv.join(', ')}`) + } + + return params as Config +})() + +export default config diff --git a/services/calendar/pod-calendar/src/error.ts b/services/calendar/pod-calendar/src/error.ts new file mode 100644 index 0000000000..585b319bb2 --- /dev/null +++ b/services/calendar/pod-calendar/src/error.ts @@ -0,0 +1,23 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export class ApiError extends Error { + constructor ( + readonly code: string, + readonly message: string + ) { + super(message) + } +} diff --git a/services/calendar/pod-calendar/src/index.ts b/services/calendar/pod-calendar/src/index.ts new file mode 100644 index 0000000000..6606ad0d66 --- /dev/null +++ b/services/calendar/pod-calendar/src/index.ts @@ -0,0 +1,20 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// eslint-disable-next-line +require('dotenv').config() +// eslint-disable-next-line +const { main } = require('./main') +void main() diff --git a/services/calendar/pod-calendar/src/jwt.ts b/services/calendar/pod-calendar/src/jwt.ts new file mode 100644 index 0000000000..a83e4c3001 --- /dev/null +++ b/services/calendar/pod-calendar/src/jwt.ts @@ -0,0 +1,21 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { encode as jwtEncode, decode as jwtDecode } from 'jwt-simple' + +import config from './config' + +export const encode = (data: any): string => jwtEncode(data, config.Secret) +export const decode = (data: string): any => jwtDecode(data, config.Secret) diff --git a/services/calendar/pod-calendar/src/main.ts b/services/calendar/pod-calendar/src/main.ts new file mode 100644 index 0000000000..3d3a050b77 --- /dev/null +++ b/services/calendar/pod-calendar/src/main.ts @@ -0,0 +1,153 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type IncomingHttpHeaders } from 'http' +import { decode64 } from './base64' +import { CalendarClient } from './calendar' +import { CalendarController } from './calendarController' +import config from './config' +import { decode } from './jwt' +import { createServer, listen } from './server' +import { closeDB, getDB } from './storage' +import { type Endpoint, type State } from './types' +import { setMetadata } from '@hcengineering/platform' +import serverClient from '@hcengineering/server-client' + +const extractToken = (header: IncomingHttpHeaders): any => { + try { + return header.authorization?.slice(7) ?? '' + } catch { + return undefined + } +} + +export const main = async (): Promise => { + setMetadata(serverClient.metadata.Endpoint, config.AccountsURL) + setMetadata(serverClient.metadata.UserAgent, config.ServiceID) + + const db = await getDB() + const calendarController = CalendarController.getCalendarController(db) + await calendarController.startAll() + const endpoints: Endpoint[] = [ + { + endpoint: '/signin', + type: 'get', + handler: async (req, res) => { + try { + const token = extractToken(req.headers) + + if (token === undefined) { + res.status(401).send() + return + } + const redirectURL = req.query.redirectURL as string + + const { email, workspace } = decode(token) + const userId = await calendarController.getUserId(email, workspace) + const url = CalendarClient.getAutUrl(redirectURL, workspace, userId, token) + res.send(url) + } catch (err) { + console.log('signin error', err) + res.status(500).send() + } + } + }, + { + endpoint: '/signin/code', + type: 'get', + handler: async (req, res) => { + const code = req.query.code as string + const state = JSON.parse(decode64(req.query.state as string)) as unknown as State + try { + await calendarController.newClient(state, code) + res.redirect(state.redirectURL) + } catch (err) { + console.log(err) + res.redirect(state.redirectURL) + } + } + }, + { + endpoint: '/signout', + type: 'get', + handler: async (req, res) => { + try { + const token = extractToken(req.headers) + + if (token === undefined) { + res.status(401).send() + return + } + + const value = req.query.value as string + + const { workspace } = decode(token) + await calendarController.signout(workspace, value) + } catch (err) { + console.log('signout error', err) + } + + res.send() + } + }, + { + endpoint: '/push', + type: 'post', + handler: async (req, res) => { + const state = req.headers['x-goog-resource-state'] + const token = req.headers['x-goog-channel-token'] + if (state === 'exists' && typeof token === 'string') { + const params = token.split('&') + const data: Record = {} + for (const param of params) { + const [key, val] = param.split('=') + if (key !== undefined && val !== undefined) { + data[key] = val + } + } + if (data.mode === undefined || data.user === undefined) { + res.status(400).send({ err: "'data' is missing" }) + return + } + calendarController.push(data.user, data.mode as 'events' | 'calendar', data.calendarId) + } + + res.send() + } + } + ] + + const server = listen(createServer(endpoints), config.Port) + + const shutdown = (): void => { + server.close(() => { + void calendarController + .close() + .then(async () => { + await closeDB() + }) + .then(() => process.exit()) + }) + } + + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + process.on('uncaughtException', (e) => { + console.error(e) + }) + process.on('unhandledRejection', (e) => { + console.error(e) + }) +} diff --git a/services/calendar/pod-calendar/src/rateLimiter.ts b/services/calendar/pod-calendar/src/rateLimiter.ts new file mode 100644 index 0000000000..ea4a986ec4 --- /dev/null +++ b/services/calendar/pod-calendar/src/rateLimiter.ts @@ -0,0 +1,34 @@ +export class RateLimiter { + private tokens: number + private lastRefillTime: number + + constructor ( + private readonly refillInterval: number, + private readonly maxTokens: number + ) { + this.tokens = maxTokens + this.lastRefillTime = Date.now() + } + + private refillTokens (): void { + const now = Date.now() + const elapsedTime = now - this.lastRefillTime + const tokensToAdd = (elapsedTime / this.refillInterval) * this.maxTokens + + this.tokens = Math.min(this.tokens + tokensToAdd, this.maxTokens) + this.lastRefillTime = now + } + + public async take (count: number): Promise { + if (count > this.maxTokens) throw new Error('Cannot take more tokens than the maximum allowed') + this.refillTokens() + + while (this.tokens < count) { + // Wait for the next refill + await new Promise((resolve) => setTimeout(resolve, this.refillInterval)) + this.refillTokens() + } + + this.tokens -= count + } +} diff --git a/services/calendar/pod-calendar/src/server.ts b/services/calendar/pod-calendar/src/server.ts new file mode 100644 index 0000000000..0ea6afbb15 --- /dev/null +++ b/services/calendar/pod-calendar/src/server.ts @@ -0,0 +1,68 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import cors from 'cors' +import express, { type Express, type NextFunction, type Request, type Response } from 'express' +import { type Server } from 'http' +import { ApiError } from './error' +import { type Endpoint, type RequestHandler } from './types' + +const catchError = (fn: RequestHandler) => (req: Request, res: Response, next: NextFunction) => { + void (async () => { + try { + await fn(req, res, next) + } catch (err: unknown) { + next(err) + } + })() +} + +export function createServer (endpoints: Endpoint[]): Express { + const app = express() + + app.use(cors()) + app.use(express.json()) + + endpoints.forEach((endpoint) => { + if (endpoint.type === 'get') { + app.get(endpoint.endpoint, catchError(endpoint.handler)) + } else if (endpoint.type === 'post') { + app.post(endpoint.endpoint, catchError(endpoint.handler)) + } + }) + + app.use((_req, res, _next) => { + res.status(404).send({ message: 'Not found' }) + }) + + app.use((err: any, _req: any, res: any, _next: any) => { + if (err instanceof ApiError) { + res.status(400).send({ code: err.code, message: err.message }) + return + } + + res.status(500).send({ message: err.message }) + }) + + return app +} + +export function listen (e: Express, port: number, host?: string): Server { + const cb = (): void => { + console.log(`Calendar service has been started at ${host ?? '*'}:${port}`) + } + + return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb) +} diff --git a/services/calendar/pod-calendar/src/storage.ts b/services/calendar/pod-calendar/src/storage.ts new file mode 100644 index 0000000000..783100494e --- /dev/null +++ b/services/calendar/pod-calendar/src/storage.ts @@ -0,0 +1,36 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MongoClient } from 'mongodb' + +import config from './config' + +let client: MongoClient | undefined +export const getDB = (() => { + return async () => { + if (client === undefined) { + client = new MongoClient(config.MongoURI) + await client.connect() + } + + return client.db(config.MongoDB) + } +})() + +export const closeDB: () => Promise = async () => { + if (client !== undefined) { + await client.close() + } +} diff --git a/services/calendar/pod-calendar/src/types.ts b/services/calendar/pod-calendar/src/types.ts new file mode 100644 index 0000000000..45b9ede9c4 --- /dev/null +++ b/services/calendar/pod-calendar/src/types.ts @@ -0,0 +1,103 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { RecurringRule } from '@hcengineering/calendar' +import type { Account, Ref, Timestamp } from '@hcengineering/core' +import type { NextFunction, Request, Response } from 'express' +import type { Credentials } from 'google-auth-library' + +export interface Watch { + timer: NodeJS.Timeout + channelId: string + resourceId: string +} + +export interface EventWatch { + timer: NodeJS.Timeout + calendarId: string + channelId: string + resourceId: string +} + +export type Token = User & Credentials + +export interface CalendarHistory { + userId: Ref + workspace: string + email: string + historyId: string +} + +export interface EventHistory { + calendarId: string + userId: Ref + workspace: string + email: string + historyId: string +} + +export interface SyncHistory { + workspace: string + timestamp: number +} + +export interface ReccuringData { + rules: RecurringRule[] + exdate: Timestamp[] + rdate: Timestamp[] +} + +export interface User { + email: string + userId: Ref + workspace: string + token: string +} + +export type State = User & { + redirectURL: string +} + +export interface AttachedFile { + size?: number + file: string + type?: string + lastModified: number + name: string +} + +export type RequestType = 'get' | 'post' + +export type RequestHandler = (req: Request, res: Response, next?: NextFunction) => Promise + +export interface Endpoint { + endpoint: string + type: RequestType + handler: RequestHandler +} + +export interface ProjectCredentials { + web: ProjectCredentialsData +} + +export interface ProjectCredentialsData { + client_id: string + project_id: string + auth_uri: string + token_uri: string + auth_provider_x509_cert_url: string + client_secret: string + redirect_uris: string[] +} diff --git a/services/calendar/pod-calendar/src/utils.ts b/services/calendar/pod-calendar/src/utils.ts new file mode 100644 index 0000000000..fa7d70185d --- /dev/null +++ b/services/calendar/pod-calendar/src/utils.ts @@ -0,0 +1,251 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { RecurringRule } from '@hcengineering/calendar' +import { Timestamp } from '@hcengineering/core' +import { ReccuringData, type Token, type User } from './types' + +export class DeferredPromise { + public readonly promise: Promise + private _resolve: (x: T) => void = () => {} + private _reject: (err: any) => void = () => {} + + constructor () { + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve + this._reject = reject + }) + } + + get resolve (): typeof this._resolve { + return this._resolve + } + + get reject (): typeof this._reject { + return this._reject + } +} + +export function isToken (user: User | Token): user is Token { + return (user as Token).access_token !== undefined +} + +export function parseRecurrenceStrings (recurrenceStrings: string[]): ReccuringData { + const res: ReccuringData = { + rdate: [], + rules: [], + exdate: [] + } + for (const str of recurrenceStrings) { + const [type, value] = str.split(':') + if (type === 'RRULE') { + res.rules.push(parseRecurrenceString(value)) + } + if (type === 'EXDATE') { + const values = value.split(',') + for (const val of values) { + try { + res.exdate.push(new Date(val).getTime()) + } catch (err) { + console.log('Convert exdate error', val) + } + } + } + if (type === 'RDATE') { + const values = value.split(',') + for (const val of values) { + try { + res.exdate.push(new Date(val).getTime()) + } catch (err) { + console.log('Convert rdate error', val) + } + } + } + } + console.log('parseRecurrenceStrings', recurrenceStrings, JSON.stringify(res)) + return res +} + +// parse 19700101T000000Z +function parseDate (dateString: string): Timestamp { + const year = parseInt(dateString.slice(0, 4)) + const month = parseInt(dateString.slice(4, 6)) - 1 + const day = parseInt(dateString.slice(6, 8)) + const hour = parseInt(dateString.slice(9, 11)) + const minute = parseInt(dateString.slice(11, 13)) + const second = parseInt(dateString.slice(13, 15)) + + return Date.UTC(year, month, day, isNaN(hour) ? 0 : hour, isNaN(minute) ? 0 : minute, isNaN(second) ? 0 : second) +} + +function parseRecurrenceString (recurrenceString: string): RecurringRule { + const rule: RecurringRule = { + freq: 'DAILY' // Default frequency + } + + const lines = recurrenceString.split(';') + + for (const line of lines) { + const [property, value] = line.split('=') + if (property !== undefined && value !== undefined) { + switch (property) { + case 'FREQ': + rule.freq = value as RecurringRule['freq'] + break + case 'UNTIL': { + const res = parseDate(value) + rule.endDate = res + break + } + case 'COUNT': + rule.count = parseInt(value, 10) + break + case 'INTERVAL': + rule.interval = parseInt(value, 10) + break + case 'BYSECOND': + rule.bySecond = value.split(',').map((s) => parseInt(s, 10)) + break + case 'BYMINUTE': + rule.byMinute = value.split(',').map((m) => parseInt(m, 10)) + break + case 'BYHOUR': + rule.byHour = value.split(',').map((h) => parseInt(h, 10)) + break + case 'BYDAY': + rule.byDay = value.split(',') + break + case 'BYMONTHDAY': + rule.byMonthDay = value.split(',').map((d) => parseInt(d, 10)) + break + case 'BYYEARDAY': + rule.byYearDay = value.split(',').map((d) => parseInt(d, 10)) + break + case 'BYWEEKNO': + rule.byWeekNo = value.split(',').map((w) => parseInt(w, 10)) + break + case 'BYMONTH': + rule.byMonth = value.split(',').map((m) => parseInt(m, 10)) + break + case 'BYSETPOS': + rule.bySetPos = value.split(',').map((s) => parseInt(s, 10)) + break + case 'WKST': + rule.wkst = value as RecurringRule['wkst'] + break + default: + // Ignore unrecognized properties + break + } + } + } + + return rule +} + +function encodeRecurringRuleToRFC5545 (rule: RecurringRule): string { + let result = `RRULE:FREQ=${rule.freq}` + + if (rule.endDate !== undefined) { + const endDate = new Date(rule.endDate) + const endDateString = endDate.toISOString().replace(/-|:|\.\d+/g, '') + result += `;UNTIL=${endDateString}` + } + + if (rule.count !== undefined) { + result += `;COUNT=${rule.count}` + } + + if (rule.interval !== undefined) { + result += `;INTERVAL=${rule.interval}` + } + + if (rule.bySecond != null && rule.bySecond.length > 0) { + result += `;BYSECOND=${rule.bySecond.join(',')}` + } + + if (rule.byMinute != null && rule.byMinute.length > 0) { + result += `;BYMINUTE=${rule.byMinute.join(',')}` + } + + if (rule.byHour != null && rule.byHour.length > 0) { + result += `;BYHOUR=${rule.byHour.join(',')}` + } + + if (rule.byDay != null && rule.byDay.length > 0) { + result += `;BYDAY=${rule.byDay.join(',')}` + } + + if (rule.byMonthDay != null && rule.byMonthDay.length > 0) { + result += `;BYMONTHDAY=${rule.byMonthDay.join(',')}` + } + + if (rule.byYearDay != null && rule.byYearDay.length > 0) { + result += `;BYYEARDAY=${rule.byYearDay.join(',')}` + } + + if (rule.byWeekNo != null && rule.byWeekNo.length > 0) { + result += `;BYWEEKNO=${rule.byWeekNo.join(',')}` + } + + if (rule.byMonth != null && rule.byMonth.length > 0) { + result += `;BYMONTH=${rule.byMonth.join(',')}` + } + + if (rule.bySetPos != null && rule.bySetPos.length > 0) { + result += `;BYSETPOS=${rule.bySetPos.join(',')}` + } + + if (rule.wkst !== undefined) { + result += `;WKST=${rule.wkst}` + } + + return result +} + +function encodeTimestampsToEXDATE (timestamps: Timestamp[]): string { + const exdateValues = timestamps.map((timestamp) => { + const date = new Date(timestamp) + return date.toISOString().replace(/-|:|\.\d+/g, '') + }) + + const exdateString = `EXDATE:${exdateValues.join(',')}` + return exdateString +} + +function encodeTimestampsToRDATE (timestamps: Timestamp[]): string { + const rdateValues = timestamps.map((timestamp) => { + const date = new Date(timestamp) + return date.toISOString().replace(/-|:|\.\d+/g, '') + }) + + const rdateString = `RDATE:${rdateValues.join(',')}` + return rdateString +} + +export function encodeReccuring (rules: RecurringRule[], rdates: number[], exdates: number[]): string[] { + const res: string[] = [] + for (const rule of rules) { + res.push(encodeRecurringRuleToRFC5545(rule)) + } + if (rdates.length > 0) { + res.push(encodeTimestampsToRDATE(rdates)) + } + if (exdates.length > 0) { + res.push(encodeTimestampsToEXDATE(exdates)) + } + + return res +} diff --git a/services/calendar/pod-calendar/src/workspaceClient.ts b/services/calendar/pod-calendar/src/workspaceClient.ts new file mode 100644 index 0000000000..fd846f99e3 --- /dev/null +++ b/services/calendar/pod-calendar/src/workspaceClient.ts @@ -0,0 +1,603 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import calendar, { Event, ExternalCalendar } from '@hcengineering/calendar' +import contact, { Channel, Contact, type Employee, type PersonAccount } from '@hcengineering/contact' +import core, { + TxProcessor, + toIdMap, + type Account, + type Client, + type Doc, + type Ref, + type Tx, + type TxCreateDoc, + type TxRemoveDoc, + type TxUpdateDoc, + TxOperations +} from '@hcengineering/core' +import setting, { Integration } from '@hcengineering/setting' +import { Collection, type Db } from 'mongodb' +import { CalendarClient } from './calendar' +import { getClient } from './client' +import config from './config' +import { encode } from './jwt' +import { SyncHistory, type ProjectCredentials, type User } from './types' +import { CalendarController } from './calendarController' + +export class WorkspaceClient { + private readonly txHandlers: ((...tx: Tx[]) => Promise)[] = [] + + private client!: Client + private readonly clients: Map = new Map() + private readonly syncHistory: Collection + private channels = new Map, Channel>() + private readonly calendarsByEmail = new Map() + readonly calendars = { + byId: new Map, ExternalCalendar>(), + byExternal: new Map() + } + + readonly contacts = { + byId: new Map, string>(), + byEmail: new Map>() + } + + readonly integrations = { + byId: new Map, Integration>(), + byContact: new Map, string[]>(), + byEmail: new Map>() + } + + private constructor ( + private readonly credentials: ProjectCredentials, + private readonly mongo: Db, + private readonly workspace: string, + private readonly serviceController: CalendarController + ) { + this.syncHistory = mongo.collection('syncHistories') + } + + static async create ( + credentials: ProjectCredentials, + mongo: Db, + workspace: string, + serviceController: CalendarController + ): Promise { + const instance = new WorkspaceClient(credentials, mongo, workspace, serviceController) + await instance.initClient(workspace) + return instance + } + + async createCalendarClient (user: User): Promise { + const current = this.getCalendarClient(user.email) + if (current !== undefined) return current + const newClient = await CalendarClient.create(this.credentials, user, this.mongo, this.client, this) + this.clients.set(user.email, newClient) + return newClient + } + + async newCalendarClient (user: User, code: string): Promise { + const newClient = await CalendarClient.create(this.credentials, user, this.mongo, this.client, this) + const email = await newClient.authorize(code) + if (this.clients.has(email)) { + await newClient.close() + throw new Error('Client already exist') + } + this.clients.set(email, newClient) + return newClient + } + + async close (): Promise { + for (const client of this.clients.values()) { + await client.close() + } + this.clients.clear() + await this.client?.close() + } + + async getUserId (email: string): Promise> { + const user = await this.client.findOne(core.class.Account, { + email + }) + if (user === undefined) { + throw new Error('User not found') + } + return user._id + } + + async signout (value: string, byError: boolean = false): Promise { + const client = this.clients.get(value) + if (client !== undefined) { + await client.signout(byError) + } else { + const integration = await this.client.findOne(setting.class.Integration, { + type: calendar.integrationType.Calendar, + value + }) + if (integration !== undefined) { + const txOp = new TxOperations(this.client, core.account.System) + if (byError) { + await txOp.update(integration, { disabled: true }) + } else { + await txOp.remove(integration) + } + } + } + this.clients.delete(value) + return this.clients.size + } + + removeClient (email: string): void { + this.clients.delete(email) + this.serviceController.removeClient(email) + if (this.clients.size > 0) return + this.serviceController.removeWorkspace(this.workspace) + } + + private getCalendarClient (email: string): CalendarClient | undefined { + return this.clients.get(email) + } + + private getCalendarClientByCalendar (id: Ref): CalendarClient | undefined { + const calendar = this.calendars.byId.get(id) + return calendar != null ? this.clients.get(calendar.externalUser) : undefined + } + + private async initClient (workspace: string): Promise { + const token = encode({ + email: config.SystemEmail, + workspace + }) + const client = await getClient(token) + client.notify = (...tx: Tx[]) => { + void this.txHandler(...tx) + } + + this.client = client + await this.init() + return this.client + } + + private async txHandler (...tx: Tx[]): Promise { + await Promise.all( + this.txHandlers.map(async (handler) => { + await handler(...tx) + }) + ) + } + + private async init (): Promise { + await this.checkUsers() + await this.initContacts() + await this.initIntegrations() + await this.initCalendars() + } + + async sync (): Promise { + await this.getNewEvents() + } + + // #region Events + + private async getSyncTime (): Promise { + const res = await this.syncHistory.findOne({ + workspace: this.workspace + }) + return res?.timestamp + } + + async updateSyncTime (): Promise { + const timestamp = Date.now() + await this.syncHistory.updateOne( + { + workspace: this.workspace + }, + { + $set: { + timestamp + } + }, + { upsert: true } + ) + } + + async getNewEvents (): Promise { + const lastSync = await this.getSyncTime() + const query = lastSync !== undefined ? { modifiedOn: { $gt: lastSync } } : {} + const newEvents = await this.client.findAll(calendar.class.Event, query) + this.txHandlers.push(async (...tx: Tx[]) => { + await this.txEventHandler(...tx) + }) + for (const newEvent of newEvents) { + const client = this.getCalendarClientByCalendar(newEvent.calendar as Ref) + if (client === undefined) { + return + } + await client.syncMyEvent(newEvent) + await this.updateSyncTime() + } + } + + private async txEventHandler (...txes: Tx[]): Promise { + for (const tx of txes) { + const actualTx = TxProcessor.extractTx(tx) + switch (actualTx._class) { + case core.class.TxCreateDoc: { + await this.txCreateEvent(actualTx as TxCreateDoc) + return + } + case core.class.TxUpdateDoc: { + await this.txUpdateEvent(actualTx as TxUpdateDoc) + return + } + case core.class.TxRemoveDoc: { + await this.txRemoveEvent(actualTx as TxRemoveDoc) + } + } + } + } + + private async txCreateEvent (tx: TxCreateDoc): Promise { + const hierarhy = this.client.getHierarchy() + if (hierarhy.isDerived(tx.objectClass, calendar.class.Event)) { + const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc) + const client = this.getCalendarClientByCalendar(doc.calendar as Ref) + if (client === undefined) { + return + } + try { + await client.createEvent(doc) + await this.updateSyncTime() + } catch (err) { + console.log(err) + } + } + } + + private async handleMove (tx: TxUpdateDoc): Promise { + const event = await this.client.findOne(calendar.class.Event, { _id: tx.objectId }) + if (event === undefined) { + return + } + try { + const txes = await this.client.findAll(core.class.TxCollectionCUD, { + 'tx.objectId': tx.objectId + }) + const extracted = txes.map((tx) => TxProcessor.extractTx(tx)).filter((p) => p._id !== tx._id) + const ev = TxProcessor.buildDoc2Doc(extracted) + if (ev !== undefined) { + const oldClient = this.getCalendarClientByCalendar(ev.calendar as Ref) + if (oldClient !== undefined) { + const oldCalendar = this.calendars.byId.get(ev.calendar as Ref) + if (oldCalendar !== undefined) { + await oldClient.remove(event.eventId, oldCalendar.externalId) + } + } + } + } catch (err) { + console.log('Error on remove event', err) + } + try { + const client = this.getCalendarClientByCalendar(event.calendar as Ref) + if (client !== undefined) { + await client.syncMyEvent(event) + } + await this.updateSyncTime() + } catch (err) { + console.log('Error on move event', err) + } + } + + private async txUpdateEvent (tx: TxUpdateDoc): Promise { + const hierarhy = this.client.getHierarchy() + if (hierarhy.isDerived(tx.objectClass, calendar.class.Event)) { + if (tx.operations.space !== undefined) { + await this.handleMove(tx as TxUpdateDoc) + return + } + const event = await this.client.findOne(calendar.class.Event, { _id: (tx as TxUpdateDoc).objectId }) + if (event === undefined) { + return + } + const client = this.getCalendarClientByCalendar(event.calendar as Ref) + if (client === undefined) { + return + } + try { + await client.updateEvent(event, tx as TxUpdateDoc) + await this.updateSyncTime() + } catch (err) { + console.log(err) + } + } + } + + private async txRemoveEvent (tx: TxRemoveDoc): Promise { + const hierarhy = this.client.getHierarchy() + if (hierarhy.isDerived(tx.objectClass, calendar.class.Event)) { + const txes = await this.client.findAll(core.class.TxCollectionCUD, { + 'tx.objectId': tx.objectId + }) + const ev = TxProcessor.buildDoc2Doc(txes.map((tx) => TxProcessor.extractTx(tx))) + if (ev === undefined) return + const client = this.getCalendarClientByCalendar(ev?.calendar as Ref) + if (client === undefined) { + return + } + await client.removeEvent(ev) + await this.updateSyncTime() + } + } + + // #endregion + + // #region Calendars + + private async initCalendars (): Promise { + const calendars = await this.client.findAll(calendar.class.ExternalCalendar, {}) + this.calendars.byId = toIdMap(calendars) + this.calendars.byExternal.clear() + this.calendarsByEmail.clear() + for (const calendar of calendars) { + const arr = this.calendarsByEmail.get(calendar.externalUser) ?? [] + arr.push(calendar) + this.calendarsByEmail.set(calendar.externalUser, arr) + const arrByExt = this.calendars.byExternal.get(calendar.externalId) ?? [] + arrByExt.push(calendar) + this.calendars.byExternal.set(calendar.externalId, arrByExt) + } + this.txHandlers.push(async (...txes: Tx[]) => { + for (const tx of txes) { + await this.txCalendarHandler(tx) + } + }) + } + + getMyCalendars (email: string): ExternalCalendar[] { + return this.calendarsByEmail.get(email) ?? [] + } + + private async txCalendarHandler (tx: Tx): Promise { + const actualTx = TxProcessor.extractTx(tx) + if (actualTx._class === core.class.TxCreateDoc) { + if ((actualTx as TxCreateDoc).objectClass === calendar.class.ExternalCalendar) { + const calendar = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) + this.calendars.byId.set(calendar._id, calendar) + const arr = this.calendars.byExternal.get(calendar.externalId) ?? [] + arr.push(calendar) + this.calendars.byExternal.set(calendar.externalId, arr) + const arrByExt = this.calendarsByEmail.get(calendar.externalUser) ?? [] + arrByExt.push(calendar) + this.calendarsByEmail.set(calendar.externalUser, arrByExt) + } + } + if (actualTx._class === core.class.TxRemoveDoc) { + const remTx = actualTx as TxRemoveDoc + const calendar = this.calendars.byId.get(remTx.objectId) + if (calendar !== undefined) { + const arr = this.calendarsByEmail.get(calendar.externalUser) ?? [] + const index = arr.findIndex((p) => p._id === calendar._id) + if (index !== -1) { + arr.splice(index, 1) + this.calendarsByEmail.set(calendar.externalUser, arr) + } + this.calendars.byId.delete(remTx.objectId) + const arrByExt = this.calendars.byExternal.get(calendar.externalId) ?? [] + const indexByExt = arrByExt.findIndex((p) => p._id === calendar._id) + if (index !== -1) { + arrByExt.splice(indexByExt, 1) + this.calendars.byExternal.set(calendar.externalId, arrByExt) + } + } + } + } + + // #endregion + + // #region Contacts + + private async initContacts (): Promise { + const channels = await this.client.findAll(contact.class.Channel, { provider: contact.channelProvider.Email }) + this.channels = toIdMap(channels) + const accounts = await this.client.findAll(contact.class.PersonAccount, {}) + for (const acc of accounts) { + this.contacts.byEmail.set(acc.email, acc.person) + this.contacts.byId.set(acc.person, acc.email) + } + for (const channel of channels) { + if (channel.value !== '') { + this.contacts.byEmail.set(channel.value, channel.attachedTo as Ref) + this.contacts.byId.set(channel.attachedTo as Ref, channel.value) + } + } + this.txHandlers.push(async (...txes: Tx[]) => { + for (const tx of txes) { + await this.txChannelHandler(tx) + } + }) + } + + private async txChannelHandler (tx: Tx): Promise { + const actualTx = TxProcessor.extractTx(tx) + if (actualTx._class === core.class.TxCreateDoc) { + if ((actualTx as TxCreateDoc).objectClass === contact.class.Channel) { + const channel = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) + if (channel.provider === contact.channelProvider.Email) { + this.contacts.byEmail.set(channel.value, channel.attachedTo as Ref) + this.contacts.byId.set(channel.attachedTo as Ref, channel.value) + this.channels.set(channel._id, channel) + } + } + } + if (actualTx._class === core.class.TxUpdateDoc) { + const updateTx = actualTx as TxUpdateDoc + if (updateTx.operations.value !== undefined) { + const channel = this.channels.get(updateTx.objectId) + if (channel !== undefined) { + const oldValue = channel.value + this.contacts.byEmail.delete(oldValue) + TxProcessor.updateDoc2Doc(channel, updateTx) + this.contacts.byEmail.set(channel.value, channel.attachedTo as Ref) + this.contacts.byId.set(channel.attachedTo as Ref, channel.value) + this.channels.set(channel._id, channel) + } + } + } + if (actualTx._class === core.class.TxRemoveDoc) { + const remTx = actualTx as TxRemoveDoc + const channel = this.channels.get(remTx.objectId) + if (channel !== undefined) { + this.contacts.byEmail.delete(channel.value) + this.contacts.byId.delete(channel.attachedTo as Ref) + this.channels.delete(channel._id) + } + } + } + + // #endregion + + // #region Integrations + + private async initIntegrations (): Promise { + const accounts = toIdMap(await this.client.findAll(contact.class.PersonAccount, {})) + const integrations = await this.client.findAll(setting.class.Integration, { + type: calendar.integrationType.Calendar + }) + for (const integration of integrations) { + const person = accounts.get((integration.createdBy ?? integration.modifiedBy) as Ref) + if (person != null) { + this.integrations.byEmail.set(integration.value, person.person) + const arr = this.integrations.byContact.get(person.person) ?? [] + arr.push(integration.value) + this.integrations.byContact.set(person.person, arr) + this.integrations.byId.set(integration._id, integration) + } + } + this.txHandlers.push(async (...txes: Tx[]) => { + for (const tx of txes) { + await this.txIntegrationHandler(tx) + } + }) + } + + private addContactIntegration (integration: Integration, account: PersonAccount): void { + const arr = this.integrations.byContact.get(account.person) ?? [] + arr.push(integration.value) + this.integrations.byContact.set(account.person, arr) + } + + private removeContactIntegration (integration: Integration, account: PersonAccount): void { + const arr = this.integrations.byContact.get(account.person) + if (arr !== undefined) { + const index = arr.findIndex((p) => p === integration.value) + if (index !== -1) { + arr.splice(index, 1) + if (arr.length > 0) { + this.integrations.byContact.set(account.person, arr) + } else { + this.integrations.byContact.delete(account.person) + } + } + } + } + + private async addIntegration (integration: Integration): Promise { + const account = await this.client.findOne(contact.class.PersonAccount, { + _id: (integration.createdBy ?? integration.modifiedBy) as Ref + }) + if (account != null) { + if (integration.value !== '') { + this.integrations.byEmail.set(integration.value, account.person) + this.addContactIntegration(integration, account) + } + this.integrations.byId.set(integration._id, integration) + } + } + + private async removeIntegration (integration: Integration): Promise { + const account = await this.client.findOne(contact.class.PersonAccount, { + _id: (integration.createdBy ?? integration.modifiedBy) as Ref + }) + if (account != null) { + this.removeContactIntegration(integration, account) + } + this.integrations.byEmail.delete(integration.value) + this.integrations.byId.delete(integration._id) + } + + private async txIntegrationHandler (tx: Tx): Promise { + const actualTx = TxProcessor.extractTx(tx) + if (actualTx._class === core.class.TxCreateDoc) { + if ((actualTx as TxCreateDoc).objectClass === setting.class.Integration) { + const integration = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) + if (integration.type === calendar.integrationType.Calendar) { + await this.addIntegration(integration) + } + } + } + if (actualTx._class === core.class.TxRemoveDoc) { + const remTx = actualTx as TxRemoveDoc + const integration = this.integrations.byId.get(remTx.objectId) + if (integration !== undefined) { + await this.removeIntegration(integration) + } + } + } + + // #endregion + + // #region Users + + async checkUsers (): Promise { + const removedEmployees = await this.client.findAll(contact.mixin.Employee, { + active: false + }) + const accounts = await this.client.findAll(contact.class.PersonAccount, { + person: { $in: removedEmployees.map((p) => p._id) } + }) + this.txHandlers.push(async (...txes: Tx[]) => { + for (const tx of txes) { + await this.txEmployeeHandler(tx) + } + }) + for (const acc of accounts) { + await this.deactivateUser(acc) + } + } + + private async deactivateUser (acc: PersonAccount): Promise { + const integrations = this.integrations.byContact.get(acc.person) ?? [] + for (const integration of integrations) { + if (integration !== '') { + await this.signout(integration, true) + } + } + } + + private async txEmployeeHandler (tx: Tx): Promise { + if (tx._class !== core.class.TxUpdateDoc) return + const ctx = tx as TxUpdateDoc + if (!this.client.getHierarchy().isDerived(ctx.objectClass, contact.class.PersonAccount)) return + if (ctx.operations.active === false) { + const acc = await this.client.findOne(contact.class.PersonAccount, { person: ctx.objectId }) + if (acc !== undefined) { + await this.deactivateUser(acc) + } + } + } + + // #endregion +} diff --git a/services/calendar/pod-calendar/tsconfig.json b/services/calendar/pod-calendar/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/calendar/pod-calendar/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/github/github-assets/.eslintrc.js b/services/github/github-assets/.eslintrc.js new file mode 100644 index 0000000000..e73094bc7e --- /dev/null +++ b/services/github/github-assets/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/assets/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/github/github-assets/assets/icons.svg b/services/github/github-assets/assets/icons.svg new file mode 100644 index 0000000000..15df30b336 --- /dev/null +++ b/services/github/github-assets/assets/icons.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/services/github/github-assets/config/rig.json b/services/github/github-assets/config/rig.json new file mode 100644 index 0000000000..b75800b9b7 --- /dev/null +++ b/services/github/github-assets/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "assets" +} diff --git a/services/github/github-assets/jest.config.js b/services/github/github-assets/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/github/github-assets/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/github/github-assets/lang/en.json b/services/github/github-assets/lang/en.json new file mode 100644 index 0000000000..8b57fcbf27 --- /dev/null +++ b/services/github/github-assets/lang/en.json @@ -0,0 +1,90 @@ +{ + "string": { + "Github": "Github", + "GithubDesc": "Github integration", + "Settings": "Configure", + "Connect": "Map repository to project", + "Configure": "Configure {title} GitHub App", + "NoIntegrationsConfigured": "

There is no {title} GitHub App associated with this workspace. You must first install the {title} GitHub App and grant it access to your repositories.

If you are installing it for a GitHub organization, you need to have the appropriate permissions, or you should ask your colleagues to install it.

If you plan like to switch use of integration to another {title} workspace, please uninstall it first.

", + "InstallApp": "Install Github App", + "ConnectionPending": "Waiting for integration service", + "Repository": "Repository", + "NoRepository": "Repository not selected", + "RepositoryInfo": "in", + "RepositoryIn": " in ", + "AssignRepository": "Assign repository", + "WithoutRepository": "Create Issue without GitHub", + "CreateGithubIssue": "Create Issue on ", + "PullRequest": "Pull request", + "PullRequests": "Pull requests", + "ChangedFiles": "Changed files ({files})", + "ReviewedFiles": "Reviewed files ({files})", + "All": "All", + "Active": "Active", + "Closed": "Closed", + "PRFile": "File", + "PRCommit": "Commit", + "PRReview": "Review", + "PRDraft": "Draft PR", + "PRMergedAt": "Merged At", + "PRClosedAt": "Closed At", + "MergeCommitSHA": "Merge commit SHA", + "ConfigLabel": "Github", + "ConfigDescription": "Bi-directional synchronization with Github", + "Authorize": "Authorize {title} GitHub App", + "ReAuthorize": "Re-authorize {title} GitHub App", + "PleaseAuthorizeAs": "You need to authorize the {title} GitHub App to access your GitHub account.", + "PleaseConnectToProject": "Please connect repository to project to start using integration", + "ConnectProject": "Connect repository (available {available})", + "Enable": "Enable", + "Disable": "Disable", + "Disabled": "Disabled", + "ProjectsInSync": "- repositories ({of}/{sync})", + "ProjectsConfig": "Github Projects (new)", + "AuthorizeAs": "as {login}", + "Integrations": "GitHub App Integrations", + "Options": "Options", + "Projects": "Projects ({count})", + "GithubIssue": "GitHub Issue", + "OnlyOrganizationError": "Only organization accounts are supported", + "Mergeable": "Ready to Merge", + "IssueRepositoryTarget": "into", + "ComponentForRepository": "Use component for Github repository", + "GithubUser": "Github", + "SyncEnabled": "Synchronization enabled", + "IssueConnectedActivityInfo": "Connected to Issue #{number} inside {repoName}", + "PullRequestConnectedActivityInfo": "Connected to Pull Request #{number} inside {repoName}", + "PullRequestMergedActivityInfo": "Pull request Merged", + "ReviewPending": "Pending", + "ReviewCommented": "Commented", + "ReviewApproved": "Approved", + "ReviewChangesRequested": "Changes requested", + "ReviewDismissed": "Dismissed", + "PullRequestMergeState": "Merge state", + "PullRequestReviewDecision": "Review state", + "Conflict": "Conflict detected", + "ReadyForMerge": "Yes", + "PROpen": "Open", + "PRMerged": "Merged", + "PRClosed": "Closed", + "AuthenticatedWithGithub": "Authorized with Github {login}", + "AuthenticatedWithGithubEmployee": "Please merge or remove Github person", + "AuthenticatedWithGithubRequired": "Please authorize Github application to act on behind you to perform actions", + "Processing": "Processing authentication/installation", + "AutoClose": "Processing complete, Tab will be auto-closed in {time} seconds.", + "PleaseRetry": "Please retry authentication", + "Updated": "Updated", + "LinkToProject": "Connect to {title}", + "UnlinkFromProject": "Disconnect", + "UnlinkRepository": "Disconnect repository from project?", + "UnlinkMessage": "

Disconnect repository {repositoryName} from {prjName}?

All synchronized issues/pull requests {total} will be not touched.

Would you like to continue?

", + "LinkedWith": "Connected with", + "SelectWorkspaceToInstallApp": "Installation should be performed from Workspace integration settings.", + "SelectWorkspaceToInstallAppMsg": "Please uninstall application and try again.", + "Uninstall": "Installed Github Apps", + "AuthenticationRevokedGithub": "Re-authorization for {login} is required for the proper functioning of the GitHub App", + "UnlinkInstallationTitle": "Uninstall Github App", + "UnlinkInstallation": "Are you sure you want to uninstall the GitHub App? Synchronization will be disabled.", + "RemoveInstallation": "Uninstall" + } +} diff --git a/services/github/github-assets/lang/pt.json b/services/github/github-assets/lang/pt.json new file mode 100644 index 0000000000..cf23c737a5 --- /dev/null +++ b/services/github/github-assets/lang/pt.json @@ -0,0 +1,90 @@ +{ + "string": { + "Github": "Github", + "GithubDesc": "Integração com o Github", + "Settings": "Configurar", + "Connect": "Mapear repositório ao projeto", + "Configure": "Configurar {title} GitHub App", + "NoIntegrationsConfigured": "

Não há {title} GitHub App associado a este workspace. Você deve primeiro instalar o {title} GitHub App e conceder acesso aos seus repositórios.

Se você estiver instalando para uma organização do GitHub, você precisa ter as permissões adequadas ou pedir para seus colegas instalarem.

Se você planeja mudar o uso da integração para outro workspace do {title}, desinstale-o primeiro.

", + "InstallApp": "Instalar Github App", + "ConnectionPending": "Aguardando serviço de integração", + "Repository": "Repositório", + "NoRepository": "Repositório não selecionado", + "RepositoryInfo": "em", + "RepositoryIn": " em ", + "AssignRepository": "Atribuir repositório", + "WithoutRepository": "Criar Issue sem o GitHub", + "CreateGithubIssue": "Criar Issue em ", + "PullRequest": "Pull request", + "PullRequests": "Pull requests", + "ChangedFiles": "Arquivos modificados ({files})", + "ReviewedFiles": "Arquivos revisados ({files})", + "All": "Todos", + "Active": "Ativo", + "Closed": "Fechado", + "PRFile": "Arquivo", + "PRCommit": "Commit", + "PRReview": "Revisão", + "PRDraft": "Rascunho PR", + "PRMergedAt": "Mergeado em", + "PRClosedAt": "Fechado em", + "MergeCommitSHA": "Merge commit SHA", + "ConfigLabel": "Github", + "ConfigDescription": "Sincronização bidirecional com o Github", + "Authorize": "Autorizar {title} GitHub App", + "ReAuthorize": "Reautorizar {title} GitHub App", + "PleaseAuthorizeAs": "Você precisa autorizar o {title} GitHub App para acessar sua conta do GitHub.", + "PleaseConnectToProject": "Por favor, conecte o repositório ao projeto para começar a usar a integração", + "ConnectProject": "Conectar repositório (disponível {available})", + "Enable": "Habilitar", + "Disable": "Desabilitar", + "Disabled": "Desabilitado", + "ProjectsInSync": "- repositórios ({of}/{sync})", + "ProjectsConfig": "Projetos do Github (novo)", + "AuthorizeAs": "como {login}", + "Integrations": "Integrações do aplicativo Huly Github", + "Options": "Opções", + "Projects": "Projetos ({count})", + "GithubIssue": "Issue do Github", + "OnlyOrganizationError": "Somente contas de organização são suportadas", + "Mergeable": "Pronto para ser mergeado", + "IssueRepositoryTarget": "em", + "ComponentForRepository": "Usar componente para repositório do Github", + "GithubUser": "Github", + "SyncEnabled": "Sincronização habilitada", + "IssueConnectedActivityInfo": "Conectado à Issue #{number} dentro de {repoName}", + "PullRequestConnectedActivityInfo": "Conectado ao Pull Request #{number} dentro de {repoName}", + "PullRequestMergedActivityInfo": "Pull request mergeado", + "ReviewPending": "Pendente", + "ReviewCommented": "Comentado", + "ReviewApproved": "Aprovado", + "ReviewChangesRequested": "Alterações solicitadas", + "ReviewDismissed": "Rejeitado", + "PullRequestMergeState": "Estado de merge", + "PullRequestReviewDecision": "Estado de revisão", + "Conflict": "Conflito detectado", + "ReadyForMerge": "Sim", + "PROpen": "Aberto", + "PRMerged": "Mergeado", + "PRClosed": "Fechado", + "AuthenticatedWithGithub": "Autorizado com o Github {login}", + "AuthenticatedWithGithubEmployee": "Por favor, faça merge ou remova a pessoa do Github", + "AuthenticatedWithGithubRequired": "Por favor, autorize a aplicação do Github a agir em seu nome para realizar ações", + "Processing": "Processando autenticação/instalação", + "AutoClose": "Processamento concluído, a guia será fechada automaticamente em {time} segundos.", + "PleaseRetry": "Por favor, tente novamente a autenticação", + "Updated": "Atualizado", + "LinkToProject": "Conectar ao {title}", + "UnlinkFromProject": "Desconectar", + "UnlinkRepository": "Desconectar repositório do projeto?", + "UnlinkMessage": "

Desconectar o repositório {repositoryName} de {prjName}?

Todas as issues/pull requests sincronizadas {total} não serão afetadas.

Deseja continuar?

", + "LinkedWith": "Conectado com", + "SelectWorkspaceToInstallApp": "A instalação deve ser feita nas configurações de integração do Workspace.", + "SelectWorkspaceToInstallAppMsg": "Por favor, desinstale o aplicativo e tente novamente.", + "Uninstall": "Aplicativos Github instalados", + "AuthenticationRevokedGithub": "É necessária uma nova autorização para {login} para o funcionamento adequado do aplicativo GitHub", + "UnlinkInstallationTitle": "Desinstalar aplicativo Github", + "UnlinkInstallation": "Tem certeza de que deseja desinstalar o aplicativo GitHub? A sincronização será desabilitada.", + "RemoveInstallation": "Desinstalar" + } +} diff --git a/services/github/github-assets/lang/ru.json b/services/github/github-assets/lang/ru.json new file mode 100644 index 0000000000..5b2f990dc0 --- /dev/null +++ b/services/github/github-assets/lang/ru.json @@ -0,0 +1,90 @@ +{ + "string": { + "Github": "Github", + "GithubDesc": "Интеграция с Github", + "Settings": "Настройки", + "Connect": "Подключить", + "Configure": "Настроить установленное приложение Github", + "NoIntegrationsConfigured": "

Не найдено {title} GitHub App, связанного с текущим пространством. Вам нужно сначала установить {title} GitHub App и предоставить ему доступ к вашим репозиториям.

Если вы устанавливаете его для GitHub организации, вам нужно иметь соответствующие права или попросить одного из ваших коллег установить приложение.

Если вы планируете переключить интеграцию в другое пространство {title}, то вам нужно сначала удалить приложение, чтобы отвязать его от предыдущего пространства.

", + "InstallApp": "Установить приложение Github", + "ConnectionPending": "Ожидание сервиса синхронизации", + "Repository": "Репозиторий", + "NoRepository": "Репозиторий не выбран", + "RepositoryInfo": "вашего", + "RepositoryIn": " в ", + "AssignRepository": "Выбрать репозиторий", + "CreateGithubIssue": "Создать Задачу в ", + "WithoutRepository": "Создать задачу без GitHub", + "PullRequest": "Пулл реквест", + "PullRequests": "Пулл реквесты", + "ChangedFiles": "Изменённые файлы ({files})", + "ReviewedFiles": "Просмотрено файлов ({files})", + "All": "Все", + "Active": "Активные", + "Closed": "Закрытие", + "PRFile": "Файл", + "PRReview": "Ревью", + "PRCommit": "Коммит", + "PRDraft": "Драфт", + "PRMergedAt": "Влито в дату", + "PRClosedAt": "Закрыто в дату", + "MergeCommitSHA": "Дайджест влитого кода", + "ConfigLabel": "Github", + "ConfigDescription": "Двусторонняя синхронизация с Github", + "Authorize": "Авторизоваться", + "ReAuthorize": "Переавторизоваться", + "PleaseAuthorizeAs": "Пожалуйста, дайте Github приложению возможность выступать от вашего имени:", + "PleaseConnectToProject": "Пожалуйста, соедините проект с репозиторием, чтобы начать использовать интеграцию", + "ConnectProject": "Подключить репозиторий (доступно {available})", + "Enable": "Включить", + "Disable": "Выключить", + "Disabled": "Выключено", + "ProjectsInSync": "- репозиториев ({of}/{sync})", + "ProjectsConfig": "Github Проекты (Новые)", + "AuthorizeAs": "Авторизован как: {login}", + "Integrations": "Huly Github интеграции", + "Options": "Опции", + "Projects": "Проекты ({count})", + "GithubIssue": "Github тикет", + "OnlyOrganizationError": "Поддерживаются только Аккаунты организации", + "Mergeable": "Готовность к влитию", + "IssueRepositoryTarget": "Репозиторий", + "ComponentForRepository": "Выберите репозиторий через компонент", + "GithubUser": "Github", + "SyncEnabled": "Синхронизация включена", + "IssueConnectedActivityInfo": "Связано с Github задачей #{number} в репозитории {repoName}", + "PullRequestConnectedActivityInfo": "Связано с Github Пулл Реквестом #{number} в репозитории {repoName}", + "PullRequestMergedActivityInfo": "Pull request Merged", + "ReviewPending": "Ревью в процессе", + "ReviewCommented": "Комментировано", + "ReviewApproved": "Одобрено", + "ReviewChangesRequested": "Требуется изменения", + "ReviewDismissed": "Отклонено", + "PullRequestMergeState": "Состояние влития", + "PullRequestReviewDecision": "Состояние Ревью", + "Conflict": "Нет, Конфликтует", + "ReadyForMerge": "Да", + "PROpen": "Открыт", + "PRMerged": "Влито", + "PRClosed": "Закрыто", + "AuthenticatedWithGithub": "Авторизация с Github успешна {login}", + "AuthenticatedWithGithubEmployee": "Пожалуйста удалите или слейте Github персону", + "AuthenticatedWithGithubRequired": "Пожалуйста авторизуйте Github приложение для действий от вашего имени", + "Processing": "Происходит авторизация/установка", + "AutoClose": "Процесс завершен, Вкладка будет автоматически закрыта через {time} секунд.", + "PleaseRetry": "Пожалуйста повторите авторизацию", + "Updated": "Обновлено", + "LinkToProject": "Подключить к {title}", + "UnlinkFromProject": "Отключить", + "UnlinkRepository": "Отключить репозиторий от проекта?", + "UnlinkMessage": "

Отключить репозиторий {repositoryName} от {prjName}?

Все синхронизованные задачи/пулл реквесты {total} останутся не тронутыми.

Желаете ли вы продолжить?

", + "LinkedWith": "Подключено к", + "SelectWorkspaceToInstallApp": "Установка должна производиться из настроек интеграции Github. ", + "SelectWorkspaceToInstallAppMsg": "Удалите приложение и повторите попытку.", + "Uninstall": "Установленное приложение Github", + "AuthenticationRevokedGithub": "Требуется повторная авторизация для {login} для правильной работы приложения GitHub", + "UnlinkInstallationTitle": "Удалить приложение Github", + "UnlinkInstallation": "Вы уверены, что хотите удалить приложение GitHub? Синхронизация будет отключена.", + "RemoveInstallation": "Удалить" + } +} diff --git a/services/github/github-assets/lang/sp.json b/services/github/github-assets/lang/sp.json new file mode 100644 index 0000000000..d94ecdf1b9 --- /dev/null +++ b/services/github/github-assets/lang/sp.json @@ -0,0 +1,90 @@ +{ + "string": { + "Github": "Github", + "GithubDesc": "Integración de Github", + "Settings": "Configurar", + "Connect": "Asociar repositorio al proyecto", + "Configure": "Configurar aplicación de GitHub {title}", + "NoIntegrationsConfigured": "

No hay una aplicación de GitHub asociada a este espacio de trabajo. Primero debes instalar la aplicación de GitHub {title} y otorgarle acceso a tus repositorios.

Si la estás instalando para una organización de GitHub, debes tener los permisos adecuados o pedir a tus colegas que la instalen.

Si deseas cambiar el uso de la integración a otro espacio de trabajo de {title}, desinstálala primero.

", + "InstallApp": "Instalar aplicación de Github", + "ConnectionPending": "Esperando el servicio de integración", + "Repository": "Repositorio", + "NoRepository": "Repositorio no seleccionado", + "RepositoryInfo": "en", + "RepositoryIn": " en ", + "AssignRepository": "Asignar repositorio", + "WithoutRepository": "Crear problema sin GitHub", + "CreateGithubIssue": "Crear problema en ", + "PullRequest": "Solicitud de extracción", + "PullRequests": "Solicitudes de extracción", + "ChangedFiles": "Archivos modificados ({files})", + "ReviewedFiles": "Archivos revisados ({files})", + "All": "Todos", + "Active": "Activo", + "Closed": "Cerrado", + "PRFile": "Archivo", + "PRCommit": "Confirmación", + "PRReview": "Revisión", + "PRDraft": "Borrador de PR", + "PRMergedAt": "Combinado en", + "PRClosedAt": "Cerrado en", + "MergeCommitSHA": "SHA de la confirmación de combinación", + "ConfigLabel": "Github", + "ConfigDescription": "Sincronización bidireccional con Github", + "Authorize": "Autorizar aplicación de GitHub {title}", + "ReAuthorize": "Volver a autorizar aplicación de GitHub {title}", + "PleaseAuthorizeAs": "Debes autorizar la aplicación de GitHub {title} para acceder a tu cuenta de GitHub.", + "PleaseConnectToProject": "Por favor, conecta el repositorio al proyecto para comenzar a usar la integración", + "ConnectProject": "Conectar repositorio (disponible {available})", + "Enable": "Habilitar", + "Disable": "Deshabilitar", + "Disabled": "Deshabilitado", + "ProjectsInSync": "- repositorios ({of}/{sync})", + "ProjectsConfig": "Proyectos de Github (nuevo)", + "AuthorizeAs": "como {login}", + "Integrations": "Integraciones de la aplicación de Github de Huly", + "Options": "Opciones", + "Projects": "Proyectos ({count})", + "GithubIssue": "Problema de Github", + "OnlyOrganizationError": "Solo se admiten cuentas de organizaciones", + "Mergeable": "Listo para combinar", + "IssueRepositoryTarget": "en", + "ComponentForRepository": "Usar componente para el repositorio de Github", + "GithubUser": "Github", + "SyncEnabled": "Sincronización habilitada", + "IssueConnectedActivityInfo": "Conectado al problema #{number} en {repoName}", + "PullRequestConnectedActivityInfo": "Conectado a la solicitud de extracción #{number} en {repoName}", + "PullRequestMergedActivityInfo": "Solicitud de extracción combinada", + "ReviewPending": "Pendiente", + "ReviewCommented": "Comentado", + "ReviewApproved": "Aprobado", + "ReviewChangesRequested": "Cambios solicitados", + "ReviewDismissed": "Descartado", + "PullRequestMergeState": "Estado de combinación", + "PullRequestReviewDecision": "Estado de revisión", + "Conflict": "Se detectó un conflicto", + "ReadyForMerge": "Sí", + "PROpen": "Abierto", + "PRMerged": "Combinado", + "PRClosed": "Cerrado", + "AuthenticatedWithGithub": "Autorizado con Github {login}", + "AuthenticatedWithGithubEmployee": "Por favor, combina o elimina a la persona de Github", + "AuthenticatedWithGithubRequired": "Por favor, autoriza la aplicación de Github para actuar en tu nombre y realizar acciones", + "Processing": "Procesando autenticación/instalación", + "AutoClose": "Procesamiento completo, la pestaña se cerrará automáticamente en {time} segundos.", + "PleaseRetry": "Por favor, vuelve a intentar la autenticación", + "Updated": "Actualizado", + "LinkToProject": "Conectar a {title}", + "UnlinkFromProject": "Desconectar", + "UnlinkRepository": "¿Desconectar el repositorio del proyecto?", + "UnlinkMessage": "

¿Desconectar el repositorio {repositoryName} de {prjName}?

Todos los problemas/solicitudes de extracción sincronizados {total} no se verán afectados.

¿Deseas continuar?

", + "LinkedWith": "Conectado con", + "SelectWorkspaceToInstallApp": "La instalación debe realizarse desde la configuración de integración del espacio de trabajo.", + "SelectWorkspaceToInstallAppMsg": "Por favor, desinstala la aplicación e intenta nuevamente.", + "Uninstall": "Aplicaciones de Github instaladas", + "AuthenticationRevokedGithub": "Se requiere una nueva autorización para {login} para el correcto funcionamiento de la aplicación de GitHub", + "UnlinkInstallationTitle": "Desinstalar aplicación de Github", + "UnlinkInstallation": "¿Estás seguro de que deseas desinstalar la aplicación de GitHub? La sincronización se deshabilitará.", + "RemoveInstallation": "Desinstalar" + } +} diff --git a/services/github/github-assets/package.json b/services/github/github-assets/package.json new file mode 100644 index 0000000000..99cfa621bc --- /dev/null +++ b/services/github/github-assets/package.json @@ -0,0 +1,38 @@ +{ + "name": "@hcengineering/github-assets", + "version": "0.6.0", + "main": "src/index.ts", + "author": "Hardcore Engineering Inc.", + "template": "@hcengineering/assets-package", + "scripts": { + "build": "compile", + "test": "jest --passWithNoTests --silent", + "build:docs": "", + "format": "format src", + "build:watch": "compile", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "eslint": "^8.54.0", + "prettier": "^3.1.0", + "@types/node": "~20.11.16", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/platform": "^0.6.11", + "@hcengineering/github": "^0.6.0" + } +} diff --git a/services/github/github-assets/src/__tests__/lang.test.ts b/services/github/github-assets/src/__tests__/lang.test.ts new file mode 100644 index 0000000000..cf96c6dcf7 --- /dev/null +++ b/services/github/github-assets/src/__tests__/lang.test.ts @@ -0,0 +1,6 @@ +import { makeLocalesTest } from '@hcengineering/platform' + +it( + 'Locales are equale', + makeLocalesTest((lang) => import(`../../lang/${lang}.json`)) +) diff --git a/services/github/github-assets/src/index.ts b/services/github/github-assets/src/index.ts new file mode 100644 index 0000000000..21d7e26bfa --- /dev/null +++ b/services/github/github-assets/src/index.ts @@ -0,0 +1,16 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +import { loadMetadata } from '@hcengineering/platform' +import github from '@hcengineering/github' + +const icons = require('../assets/icons.svg') as string // eslint-disable-line +loadMetadata(github.icon, { + Github: `${icons}#github`, + GithubRepository: `${icons}#repository`, + PullRequest: `${icons}#pullRequest`, + PullRequestMerged: `${icons}#pullRequestMerged`, + PullRequestClosed: `${icons}#pullRequestClosed`, + Forks: `${icons}#forks` +}) diff --git a/services/github/github-assets/tsconfig.json b/services/github/github-assets/tsconfig.json new file mode 100644 index 0000000000..4449c73536 --- /dev/null +++ b/services/github/github-assets/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/assets/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "types": ["node", "jest"], + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/github/github-resources/.eslintrc.js b/services/github/github-resources/.eslintrc.js new file mode 100644 index 0000000000..bb8fd7450d --- /dev/null +++ b/services/github/github-resources/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'], + parserOptions: { tsconfigRootDir: __dirname } +} diff --git a/services/github/github-resources/.prettierrc b/services/github/github-resources/.prettierrc new file mode 100644 index 0000000000..792942803a --- /dev/null +++ b/services/github/github-resources/.prettierrc @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "trailingComma": "none", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 120, + "useTabs": false, + "bracketSpacing": true, + "proseWrap": "preserve", + "plugins": [ + "prettier-plugin-svelte" + ], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} \ No newline at end of file diff --git a/services/github/github-resources/config/rig.json b/services/github/github-resources/config/rig.json new file mode 100644 index 0000000000..bcad6f7c33 --- /dev/null +++ b/services/github/github-resources/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "ui" +} diff --git a/services/github/github-resources/jest.config.js b/services/github/github-resources/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/github/github-resources/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/github/github-resources/package.json b/services/github/github-resources/package.json new file mode 100644 index 0000000000..d9a9c4090a --- /dev/null +++ b/services/github/github-resources/package.json @@ -0,0 +1,71 @@ +{ + "name": "@hcengineering/github-resources", + "version": "0.6.0", + "main": "src/index.ts", + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile ui", + "build:docs": "api-extractor run --local", + "format": "format src", + "build:watch": "compile ui", + "_phase:build": "compile ui", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "svelte-loader": "^3.2.0", + "sass": "^1.53.0", + "svelte-preprocess": "^5.1.3", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier-plugin-svelte": "^3.2.2", + "prettier": "^3.1.0", + "svelte-check": "^3.6.9", + "typescript": "^5.3.3", + "@types/qs": "~6.9.7", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "eslint-plugin-svelte": "^2.35.1", + "svelte-eslint-parser": "^0.33.1" + }, + "dependencies": { + "@hcengineering/platform": "^0.6.11", + "svelte": "^4.2.12", + "@hcengineering/github": "^0.6.0", + "@hcengineering/ui": "^0.6.15", + "@hcengineering/presentation": "^0.6.3", + "@hcengineering/text": "^0.6.5", + "@hcengineering/text-editor": "^0.6.0", + "@hcengineering/text-editor-resources": "^0.6.0", + "@hcengineering/analytics": "^0.6.0", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/contact-resources": "^0.6.0", + "@hcengineering/setting": "^0.6.17", + "@hcengineering/login": "^0.6.12", + "@hcengineering/core": "^0.6.32", + "@hcengineering/view": "^0.6.13", + "@hcengineering/view-resources": "^0.6.0", + "@hcengineering/setting-resources": "^0.6.0", + "@hcengineering/client": "^0.6.18", + "@hcengineering/chunter": "^0.6.20", + "fast-equals": "^5.0.1", + "@hcengineering/task": "^0.6.20", + "@hcengineering/tracker": "^0.6.24", + "@hcengineering/panel": "^0.6.23", + "@hcengineering/attachment-resources": "^0.6.0", + "@hcengineering/chunter-resources": "^0.6.0", + "@hcengineering/notification": "^0.6.23", + "@hcengineering/tracker-resources": "^0.6.0", + "@hcengineering/preference": "^0.6.13", + "@hcengineering/diffview": "^0.6.0", + "@hcengineering/activity": "^0.6.0", + "@hcengineering/activity-resources": "^0.6.1" + } +} diff --git a/services/github/github-resources/postcss.config.js b/services/github/github-resources/postcss.config.js new file mode 100644 index 0000000000..88752c6cb0 --- /dev/null +++ b/services/github/github-resources/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + require('autoprefixer') + ] +} diff --git a/services/github/github-resources/src/components/AuthenticationCheck.svelte b/services/github/github-resources/src/components/AuthenticationCheck.svelte new file mode 100644 index 0000000000..4c2f6a9fae --- /dev/null +++ b/services/github/github-resources/src/components/AuthenticationCheck.svelte @@ -0,0 +1,42 @@ + + +{#if spaceObj !== undefined} + {#if auth === undefined || auth.login === '' || auth.error != null} + {#if !readonly} + { + void onAuthorize() + }} + /> + {/if} + {:else} + + {/if} +{/if} diff --git a/services/github/github-resources/src/components/Configure.svelte b/services/github/github-resources/src/components/Configure.svelte new file mode 100644 index 0000000000..e674473c62 --- /dev/null +++ b/services/github/github-resources/src/components/Configure.svelte @@ -0,0 +1,8 @@ + + + diff --git a/services/github/github-resources/src/components/Connect.svelte b/services/github/github-resources/src/components/Connect.svelte new file mode 100644 index 0000000000..46cce16a99 --- /dev/null +++ b/services/github/github-resources/src/components/Connect.svelte @@ -0,0 +1,174 @@ + + + + dispatch('close')} + on:changeContent +> + {#if loading} + + {:else} + { + selectedTab = result.detail.id + }} + /> +
+ {#if selectedTab === 'personal'} +
+ {#if auth} + + {:else} +
+ {:else if selectedTab === 'installations'} +
+ +
+ {#if integrations.length === 0} +
+
+ {#await translate(github.string.NoIntegrationsConfigured, { appName: getMetadata(github.metadata.GithubApplication), title: getMetadata(ui.metadata.PlatformTitle) }) then msg} + + {/await} +
+ + + +
+
+
+ {/if} +
+
+ {/if} +
+ {/if} + + {#if selectedTab === 'personal'} + {#if auth !== undefined && $ticker - (auth?.authRequestTime ?? 0) > 0 && auth.login === ''} +
+
+ {/if} + + + + {#if !isCollapsed} + +
+ { + const { fileName, sha, viewed } = evt.detail + handleFileViewed(fileName, sha, viewed) + }} + /> +
+
+ {/if} + {/if} + + + diff --git a/services/github/github-resources/src/components/PullRequestMergeState.svelte b/services/github/github-resources/src/components/PullRequestMergeState.svelte new file mode 100644 index 0000000000..268f76e68c --- /dev/null +++ b/services/github/github-resources/src/components/PullRequestMergeState.svelte @@ -0,0 +1,44 @@ + + +{#if pr} + {#if pr.state === GithubPullRequestState.open} +
+ +
+ + {#if pr.mergeable === 'CONFLICTING' && !small} +
+
+ {/if} + {:else if pr.state === GithubPullRequestState.merged} +
+ + {#if !small} +
+ {:else if pr.state === GithubPullRequestState.closed} +
+ + {#if !small} +
+ {/if} +{/if} diff --git a/services/github/github-resources/src/components/PullRequests.svelte b/services/github/github-resources/src/components/PullRequests.svelte new file mode 100644 index 0000000000..a19765ffea --- /dev/null +++ b/services/github/github-resources/src/components/PullRequests.svelte @@ -0,0 +1,84 @@ + + + +{#if query !== undefined && modeSelectorProps !== undefined} + {#key query && currentSpace} + + {/key} +{/if} diff --git a/services/github/github-resources/src/components/PullRequestsView.svelte b/services/github/github-resources/src/components/PullRequestsView.svelte new file mode 100644 index 0000000000..fe61335c0d --- /dev/null +++ b/services/github/github-resources/src/components/PullRequestsView.svelte @@ -0,0 +1,95 @@ + + + + + + + + + {#if asideFloat && $$slots.aside} +
+ +{:else} + +{/if} diff --git a/services/github/github-resources/src/components/RepositorySelector.svelte b/services/github/github-resources/src/components/RepositorySelector.svelte new file mode 100644 index 0000000000..d4d7561d44 --- /dev/null +++ b/services/github/github-resources/src/components/RepositorySelector.svelte @@ -0,0 +1,111 @@ + + + + { + if (ev.detail != null) { + value = ev.detail + } + }} +> + + {#if selectedRepository !== undefined} +
+ +
+ {selectedRepository.name} + {/if} +
+
diff --git a/services/github/github-resources/src/components/languageColors.ts b/services/github/github-resources/src/components/languageColors.ts new file mode 100644 index 0000000000..346d48e382 --- /dev/null +++ b/services/github/github-resources/src/components/languageColors.ts @@ -0,0 +1,200 @@ +export const githubLanguageColors: Record = { + ABAP: '#E8274B', + ActionScript: '#882B0F', + Ada: '#02f88c', + Agda: '#315665', + 'AGS Script': '#B9D9FF', + Alloy: '#64C800', + AMPL: '#E6EFBB', + ANTLR: '#9DC3FF', + 'API Blueprint': '#2ACCA8', + APL: '#5A8164', + Arc: '#aa2afe', + Arduino: '#bd79d1', + ASP: '#6a40fd', + AspectJ: '#a957b0', + Assembly: '#6E4C13', + ATS: '#1ac620', + AutoHotkey: '#6594b9', + AutoIt: '#1C3552', + BlitzMax: '#cd6400', + Boo: '#d4bec1', + Brainfuck: '#2F2530', + 'C Sharp': '#178600', + C: '#555555', + Chapel: '#8dc63f', + Cirru: '#ccccff', + Clarion: '#db901e', + Clean: '#3F85AF', + Click: '#E4E6F3', + Clojure: '#db5855', + CoffeeScript: '#244776', + 'ColdFusion CFC': '#ed2cd6', + ColdFusion: '#ed2cd6', + 'Common Lisp': '#3fb68b', + 'Component Pascal': '#b0ce4e', + cpp: '#f34b7d', + Crystal: '#776791', + CSS: '#563d7c', + D: '#ba595e', + Dart: '#00B4AB', + Diff: '#88dddd', + DM: '#447265', + Dogescript: '#cca760', + Dylan: '#6c616e', + E: '#ccce35', + Eagle: '#814C05', + eC: '#913960', + ECL: '#8a1267', + edn: '#db5855', + Eiffel: '#946d57', + Elixir: '#6e4a7e', + Elm: '#60B5CC', + 'Emacs Lisp': '#c065db', + EmberScript: '#FFF4F3', + Erlang: '#B83998', + 'F#': '#b845fc', + Factor: '#636746', + Fancy: '#7b9db4', + Fantom: '#dbded5', + FLUX: '#88ccff', + Forth: '#341708', + FORTRAN: '#4d41b1', + FreeMarker: '#0050b2', + Frege: '#00cafe', + 'Game Maker Language': '#8fb200', + Glyph: '#e4cc98', + Gnuplot: '#f0a9f0', + Go: '#375eab', + Golo: '#88562A', + Gosu: '#82937f', + 'Grammatical Framework': '#79aa7a', + Groovy: '#e69f56', + Handlebars: '#01a9d6', + Harbour: '#0e60e3', + Haskell: '#29b544', + Haxe: '#df7900', + HTML: '#e44b23', + Hy: '#7790B2', + IDL: '#a3522f', + Io: '#a9188d', + Ioke: '#078193', + Isabelle: '#FEFE00', + J: '#9EEDFF', + Java: '#b07219', + JavaScript: '#f1e05a', + JFlex: '#DBCA00', + JSONiq: '#40d47e', + Julia: '#a270ba', + 'Jupyter Notebook': '#DA5B0B', + Kotlin: '#F18E33', + KRL: '#28431f', + Lasso: '#999999', + Latte: '#A8FF97', + Lex: '#DBCA00', + LFE: '#004200', + LiveScript: '#499886', + LOLCODE: '#cc9900', + LookML: '#652B81', + LSL: '#3d9970', + Lua: '#000080', + Makefile: '#427819', + Mask: '#f97732', + Matlab: '#bb92ac', + Max: '#c4a79c', + MAXScript: '#00a6a6', + Mercury: '#ff2b2b', + Metal: '#8f14e9', + Mirah: '#c7a938', + MTML: '#b7e1f4', + NCL: '#28431f', + Nemerle: '#3d3c6e', + nesC: '#94B0C7', + NetLinx: '#0aa0ff', + 'NetLinx+ERB': '#747faa', + NetLogo: '#ff6375', + NewLisp: '#87AED7', + Nimrod: '#37775b', + Nit: '#009917', + Nix: '#7e7eff', + Nu: '#c9df40', + 'Objective-C': '#438eff', + 'Objective-C++': '#6866fb', + 'Objective-J': '#ff0c5a', + OCaml: '#3be133', + Omgrofl: '#cabbff', + ooc: '#b0b77e', + Opal: '#f7ede0', + Oxygene: '#cdd0e3', + Oz: '#fab738', + Pan: '#cc0000', + Papyrus: '#6600cc', + Parrot: '#f3ca0a', + Pascal: '#b0ce4e', + PAWN: '#dbb284', + Perl: '#0298c3', + Perl6: '#0000fb', + PHP: '#4F5D95', + PigLatin: '#fcd7de', + Pike: '#005390', + PLSQL: '#dad8d8', + PogoScript: '#d80074', + Processing: '#0096D8', + Prolog: '#74283c', + 'Propeller Spin': '#7fa2a7', + Puppet: '#302B6D', + 'Pure Data': '#91de79', + PureBasic: '#5a6986', + PureScript: '#1D222D', + Python: '#3572A5', + QML: '#44a51c', + R: '#198ce7', + Racket: '#22228f', + 'Ragel in Ruby Host': '#9d5200', + RAML: '#77d9fb', + Rebol: '#358a5b', + Red: '#ee0000', + "Ren'Py": '#ff7f7f', + Rouge: '#cc0088', + Ruby: '#701516', + Rust: '#dea584', + SaltStack: '#646464', + SAS: '#B34936', + Scala: '#DC322F', + Scheme: '#1e4aec', + Self: '#0579aa', + Shell: '#89e051', + Shen: '#120F14', + Slash: '#007eff', + Slim: '#ff8f77', + Smalltalk: '#596706', + SourcePawn: '#5c7611', + SQF: '#3F3F3F', + Squirrel: '#800000', + Stan: '#b2011d', + 'Standard ML': '#dc566d', + SuperCollider: '#46390b', + Swift: '#ffac45', + SystemVerilog: '#DAE1C2', + Tcl: '#e4cc98', + TeX: '#3D6117', + Turing: '#45f715', + TypeScript: '#2b7489', + 'Unified Parallel C': '#4e3617', + 'Unity3D Asset': '#ab69a1', + UnrealScript: '#a54c4d', + Vala: '#fbe5cd', + Verilog: '#b2b7f8', + VHDL: '#adb2cb', + VimL: '#199f4b', + 'Visual Basic': '#945db7', + Volt: '#1F1F1F', + Vue: '#2c3e50', + 'Web Ontology Language': '#9cc9dd', + wisp: '#7582D1', + X10: '#4B6BEF', + xBase: '#403a40', + XC: '#99DA07', + XQuery: '#5232e7', + Zephir: '#118f9e' +} diff --git a/services/github/github-resources/src/components/presenters/GithubIssuePresenter.svelte b/services/github/github-resources/src/components/presenters/GithubIssuePresenter.svelte new file mode 100644 index 0000000000..6e37564a17 --- /dev/null +++ b/services/github/github-resources/src/components/presenters/GithubIssuePresenter.svelte @@ -0,0 +1,27 @@ + + +{#if ghIssue !== undefined && ghIssue.url !== '' && repository !== undefined} +
+ +
+{/if} diff --git a/services/github/github-resources/src/components/presenters/GithubReviewPresenter.svelte b/services/github/github-resources/src/components/presenters/GithubReviewPresenter.svelte new file mode 100644 index 0000000000..b06a6803eb --- /dev/null +++ b/services/github/github-resources/src/components/presenters/GithubReviewPresenter.svelte @@ -0,0 +1,112 @@ + + + +{#if value?.state !== GithubPullRequestReviewState.Commented || (value?.state === GithubPullRequestReviewState.Commented && (value?.body?.length ?? 0) > 0 && !isEmptyMarkup(value?.body ?? ''))} +
+ + + + + +
+
+ +
+
+
+
+
+{/if} + + diff --git a/services/github/github-resources/src/components/presenters/GithubReviewThreadPresenter.svelte b/services/github/github-resources/src/components/presenters/GithubReviewThreadPresenter.svelte new file mode 100644 index 0000000000..2cc9984bdd --- /dev/null +++ b/services/github/github-resources/src/components/presenters/GithubReviewThreadPresenter.svelte @@ -0,0 +1,190 @@ + + + +
+ + + + + +
+ {#if comments.length > 0} + { + expanded = value + } + }} + /> + {/if} + {#if expanded} +
+ {#each comments as comment} + + {/each} +
+ +
+ {#if githubConfiguration.ResolveThreadSupported} +
+ {/if} +
+
+
+
+ + diff --git a/services/github/github-resources/src/components/presenters/MergeableValuePresenter.svelte b/services/github/github-resources/src/components/presenters/MergeableValuePresenter.svelte new file mode 100644 index 0000000000..0e6533a8d1 --- /dev/null +++ b/services/github/github-resources/src/components/presenters/MergeableValuePresenter.svelte @@ -0,0 +1,16 @@ + + +
+ {#if value === PullRequestMergeable.CONFLICTING} +
diff --git a/services/github/github-resources/src/components/presenters/PullRequestNotificationPresenter.svelte b/services/github/github-resources/src/components/presenters/PullRequestNotificationPresenter.svelte new file mode 100644 index 0000000000..376c2c07c0 --- /dev/null +++ b/services/github/github-resources/src/components/presenters/PullRequestNotificationPresenter.svelte @@ -0,0 +1,42 @@ + + + +{#if value} +
+
+ {value.identifier} + + {currentProject?.name} + +
+ + {value.title} + {#if ghIssue} + # {ghIssue.githubNumber} + {/if} + +
+{/if} diff --git a/services/github/github-resources/src/components/presenters/PullRequestPresenter.svelte b/services/github/github-resources/src/components/presenters/PullRequestPresenter.svelte new file mode 100644 index 0000000000..44fa98aa7c --- /dev/null +++ b/services/github/github-resources/src/components/presenters/PullRequestPresenter.svelte @@ -0,0 +1,29 @@ + + + + diff --git a/services/github/github-resources/src/components/presenters/PullRequestReviewDecisionValuePresenter.svelte b/services/github/github-resources/src/components/presenters/PullRequestReviewDecisionValuePresenter.svelte new file mode 100644 index 0000000000..17e6cdab38 --- /dev/null +++ b/services/github/github-resources/src/components/presenters/PullRequestReviewDecisionValuePresenter.svelte @@ -0,0 +1,52 @@ + + +
+
+ + {#if !small} +
+
+ {/if} +
+
diff --git a/services/github/github-resources/src/components/presenters/PullRequestStateValuePresenter.svelte b/services/github/github-resources/src/components/presenters/PullRequestStateValuePresenter.svelte new file mode 100644 index 0000000000..411890bfbb --- /dev/null +++ b/services/github/github-resources/src/components/presenters/PullRequestStateValuePresenter.svelte @@ -0,0 +1,18 @@ + + +
+ {#if value === GithubPullRequestState.open} +
diff --git a/services/github/github-resources/src/components/presenters/RepositoryPresenter.svelte b/services/github/github-resources/src/components/presenters/RepositoryPresenter.svelte new file mode 100644 index 0000000000..7f367c3d30 --- /dev/null +++ b/services/github/github-resources/src/components/presenters/RepositoryPresenter.svelte @@ -0,0 +1,17 @@ + + + +
+ + {value.name} +
diff --git a/services/github/github-resources/src/components/presenters/ReviewCommentPresenter.svelte b/services/github/github-resources/src/components/presenters/ReviewCommentPresenter.svelte new file mode 100644 index 0000000000..f4ec11eddc --- /dev/null +++ b/services/github/github-resources/src/components/presenters/ReviewCommentPresenter.svelte @@ -0,0 +1,58 @@ + + +{#if comment} +
+
+
+ {#if $$slots.icon} + + {:else if person} + + {:else} + + {/if} +
+
+ {#if person} + + {:else} +
+
+ {/if} + + {getDisplayTime(comment.createdOn ?? 0)} +
+
+
+ +
+
+{/if} + + diff --git a/services/github/github-resources/src/components/presenters/TitlePresenter.svelte b/services/github/github-resources/src/components/presenters/TitlePresenter.svelte new file mode 100644 index 0000000000..1a7c3200ef --- /dev/null +++ b/services/github/github-resources/src/components/presenters/TitlePresenter.svelte @@ -0,0 +1,21 @@ + + + +{#if value} + +{/if} diff --git a/services/github/github-resources/src/components/utils.ts b/services/github/github-resources/src/components/utils.ts new file mode 100644 index 0000000000..2a49911c2c --- /dev/null +++ b/services/github/github-resources/src/components/utils.ts @@ -0,0 +1,111 @@ +import { Analytics } from '@hcengineering/analytics' +import core, { concatLink, getCurrentAccount, toIdMap, type IdMap } from '@hcengineering/core' +import { PlatformError, getMetadata, unknownError } from '@hcengineering/platform' +import presentation, { createQuery, getClient } from '@hcengineering/presentation' +import { location } from '@hcengineering/ui' +import { + makeQuery, + type GithubAuthentication, + type GithubIntegrationRepository, + type GithubProject +} from '@hcengineering/github' +import { get, writable } from 'svelte/store' +import github from '../plugin' + +export async function onAuthorize (login?: string): Promise { + const meId = getCurrentAccount()._id + const state = btoa( + JSON.stringify({ + accountId: meId, + workspace: get(location).path[1], + token: getMetadata(presentation.metadata.Token), + op: 'authorize' + }) + ) + const client = getClient() + + const config = await client.findOne(github.class.GithubAuthentication, {}) + + if (config !== undefined) { + await client.remove(config) + } + await client.createDoc(github.class.GithubAuthentication, core.space.Workspace, { + attachedTo: meId, + login: '', + error: null, + authRequestTime: Date.now(), + createdAt: new Date(), + followers: 0, + following: 0, + nodeId: '', + updatedAt: new Date(), + url: '', + repositories: 0, + organizations: { totalCount: 0, nodes: [] }, + closedIssues: 0, + openIssues: 0, + mergedPRs: 0, + openPRs: 0, + closedPRs: 0, + repositoryDiscussions: 0, + starredRepositories: 0 + }) + + Analytics.handleEvent('Authorize github clicked') + + const url = + 'https://github.com/login/oauth/authorize?' + + makeQuery({ + client_id: getMetadata(github.metadata.GithubClientID), + login: '', + state, + allow_signup: 'true' + }) + window.open(url) +} + +const repositoryQuery = createQuery(true) + +/** + * @public + */ +export const integrationRepositories = writable>(new Map()) + +repositoryQuery.query(github.class.GithubIntegrationRepository, {}, (res) => { + integrationRepositories.set(toIdMap(res)) +}) + +const projectQuery = createQuery(true) + +/** + * @public + */ +export const githubProjects = writable>(new Map()) + +projectQuery.query(github.mixin.GithubProject, {}, (res) => { + githubProjects.set(toIdMap(res)) +}) + +const authQuery = createQuery(true) +export const githubAuth = writable(undefined) +authQuery.query(github.class.GithubAuthentication, {}, (res) => { + githubAuth.set(res.shift()) +}) + +/** + * @public + */ +export async function sendGHServiceRequest (path: string, args: Record): Promise { + const githubURL = getMetadata(github.metadata.GithubURL) + if (githubURL === undefined) { + // We could try use recognition service to find some document properties. + throw new PlatformError(unknownError('Github integration is not configured')) + } + return await fetch(concatLink(concatLink(githubURL, '/api/v1/'), path), { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(args) + }) +} diff --git a/services/github/github-resources/src/configuration.ts b/services/github/github-resources/src/configuration.ts new file mode 100644 index 0000000000..5eb37732e9 --- /dev/null +++ b/services/github/github-resources/src/configuration.ts @@ -0,0 +1,6 @@ +/** + * @public + */ +export const githubConfiguration = { + ResolveThreadSupported: false +} diff --git a/services/github/github-resources/src/index.ts b/services/github/github-resources/src/index.ts new file mode 100644 index 0000000000..e706a87ec1 --- /dev/null +++ b/services/github/github-resources/src/index.ts @@ -0,0 +1,90 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +import { type Data, type Ref, type Space, type TxOperations } from '@hcengineering/core' +import { type Resources } from '@hcengineering/platform' +import { getClient, type DocCreateFunction, type DocCreatePhase } from '@hcengineering/presentation' +import tracker, { type Issue } from '@hcengineering/tracker' +import { type GithubIntegrationRepository } from '@hcengineering/github' +import AuthenticationCheck from './components/AuthenticationCheck.svelte' +import GithubIssueHeader from './components/GithubIssueHeader.svelte' +import Configure from './components/Configure.svelte' +import Connect from './components/Connect.svelte' +import ConnectApp from './components/ConnectApp.svelte' +import EditPullRequest from './components/EditPullRequest.svelte' +import GithubIcon from './components/GithubIcon.svelte' +import GithubIssueInfo from './components/GithubIssueInfo.svelte' +import GithubIssueInfoHeader from './components/GithubIssueInfoHeader.svelte' +import PullRequestMergeState from './components/PullRequestMergeState.svelte' +import PullRequests from './components/PullRequests.svelte' +import RepositoryPresenterRef from './components/RepositoryPresenterRef.svelte' +import RepositoryPresenterRefEditor from './components/RepositoryPresenterRefEditor.svelte' +import GithubReviewPresenter from './components/presenters/GithubReviewPresenter.svelte' +import GithubReviewThreadPresenter from './components/presenters/GithubReviewThreadPresenter.svelte' +import MergeableValuePresenter from './components/presenters/MergeableValuePresenter.svelte' +import PullRequestNotificationPresenter from './components/presenters/PullRequestNotificationPresenter.svelte' +import PullRequestPresenter from './components/presenters/PullRequestPresenter.svelte' +import PullRequestReviewDecisionValuePresenter from './components/presenters/PullRequestReviewDecisionValuePresenter.svelte' +import PullRequestStateValuePresenter from './components/presenters/PullRequestStateValuePresenter.svelte' +import RepositoryPresenter from './components/presenters/RepositoryPresenter.svelte' +import TitlePresenter from './components/presenters/TitlePresenter.svelte' +import GithubIssuePresenter from './components/presenters/GithubIssuePresenter.svelte' +import github from './plugin' + +async function updateIssue ( + client: TxOperations, + id: Ref, + space: Space, + issue: Data, + settings: Record, + phase: DocCreatePhase +): Promise { + // We should add mixin only in post phase. + if (client.getHierarchy().hasMixin(space, github.mixin.GithubProject) && phase === 'post') { + // No actions required for now. + await client.createMixin(id, tracker.class.Issue, space._id, github.mixin.GithubIssue, { + githubNumber: 0, + repository: settings.repository as Ref, + url: '' + }) + } +} + +export default async (): Promise => ({ + component: { + Connect, + Configure, + GithubIcon, + ConnectApp, + PullRequestPresenter, + PullRequestNotificationPresenter, + PullRequests, + EditPullRequest, + GithubReviewPresenter, + GithubReviewThreadPresenter, + TitlePresenter, + RepositoryPresenter, + RepositoryPresenterRef, + RepositoryPresenterRefEditor, + AuthenticationCheck, + GithubIssueHeader, + GithubIssueInfo, + GithubIssueInfoHeader, + MergeableValuePresenter, + PullRequestReviewDecisionValuePresenter, + PullRequestStateValuePresenter, + PullRequestMergeState, + GithubIssuePresenter + }, + handler: { + DisconnectHandler: async () => {} + }, + functions: { + ShowForRepositoryOnly: async (spaces: Space[]) => { + const client = getClient() + return spaces.some((it) => client.getHierarchy().hasMixin(it, github.mixin.GithubProject)) + }, + UpdateIssue: updateIssue as DocCreateFunction + } +}) diff --git a/services/github/github-resources/src/plugin.ts b/services/github/github-resources/src/plugin.ts new file mode 100644 index 0000000000..07144e2c1b --- /dev/null +++ b/services/github/github-resources/src/plugin.ts @@ -0,0 +1,101 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +import { type IntlString, mergeIds } from '@hcengineering/platform' + +import github, { githubId } from '@hcengineering/github' +import { type Ref } from '@hcengineering/core' +import { type Handler, type IntegrationType } from '@hcengineering/setting' +import { type AnyComponent } from '@hcengineering/ui' + +export default mergeIds(githubId, github, { + string: { + Github: '' as IntlString, + GithubDesc: '' as IntlString, + Settings: '' as IntlString, + Connect: '' as IntlString, + Configure: '' as IntlString, + InstallApp: '' as IntlString, + NoIntegrationsConfigured: '' as IntlString, + ConnectionPending: '' as IntlString, + Repository: '' as IntlString, + NoRepository: '' as IntlString, + RepositoryInfo: '' as IntlString, + WithoutRepository: '' as IntlString, + RepositoryIn: '' as IntlString, + AssignRepository: '' as IntlString, + CreateGithubIssue: '' as IntlString, + PullRequests: '' as IntlString, + ChangedFiles: '' as IntlString, + ReviewedFiles: '' as IntlString, + All: '' as IntlString, + Active: '' as IntlString, + Closed: '' as IntlString, + Authorize: '' as IntlString, + ReAuthorize: '' as IntlString, + PleaseConnectToProject: '' as IntlString, + ConnectProject: '' as IntlString, + Enable: '' as IntlString, + Disable: '' as IntlString, + Disabled: '' as IntlString, + ProjectsInSync: '' as IntlString, + ProjectsConfig: '' as IntlString, + AuthorizeAs: '' as IntlString, + PleaseAuthorizeAs: '' as IntlString, + Integrations: '' as IntlString, + Options: '' as IntlString, + Projects: '' as IntlString, + OnlyOrganizationError: '' as IntlString, + IssueRepositoryTarget: '' as IntlString, + ComponentForRepository: '' as IntlString, + SyncEnabled: '' as IntlString, + ReviewPending: '' as IntlString, + ReviewCommented: '' as IntlString, + ReviewApproved: '' as IntlString, + ReviewChangesRequested: '' as IntlString, + ReviewDismissed: '' as IntlString, + + Conflict: '' as IntlString, + ReadyForMerge: '' as IntlString, + PROpen: '' as IntlString, + PRMerged: '' as IntlString, + PRClosed: '' as IntlString, + + InfoLogin: '' as IntlString, + + Processing: '' as IntlString, + AutoClose: '' as IntlString, + PleaseRetry: '' as IntlString, + Updated: '' as IntlString, + LinkToProject: '' as IntlString, + UnlinkFromProject: '' as IntlString, + LinkedWith: '' as IntlString, + + UnlinkRepository: '' as IntlString, + UnlinkMessage: '' as IntlString, + UnlinkInstallationTitle: '' as IntlString, + UnlinkInstallation: '' as IntlString, + RemoveInstallation: '' as IntlString, + SelectWorkspaceToInstallApp: '' as IntlString, + SelectWorkspaceToInstallAppMsg: '' as IntlString, + Uninstall: '' as IntlString + }, + component: { + GithubIcon: '' as AnyComponent, + EditPullRequest: '' as AnyComponent, + PullRequests: '' as AnyComponent, + RepositoryPresenter: '' as AnyComponent, + RepositoryPresenterRef: '' as AnyComponent, + RepositoryPresenterRefEditor: '' as AnyComponent, + GithubIssueInfo: '' as AnyComponent, + GithubIssueInfoHeader: '' as AnyComponent, + GithubIssuePresenter: '' as AnyComponent + }, + handler: { + DisconnectHandler: '' as Handler + }, + integrationType: { + Github: '' as Ref + } +}) diff --git a/services/github/github-resources/svelte.config.js b/services/github/github-resources/svelte.config.js new file mode 100644 index 0000000000..944a06f73e --- /dev/null +++ b/services/github/github-resources/svelte.config.js @@ -0,0 +1,5 @@ +const sveltePreprocess = require('svelte-preprocess') + +module.exports = { + preprocess: sveltePreprocess() +}; \ No newline at end of file diff --git a/services/github/github-resources/tsconfig.json b/services/github/github-resources/tsconfig.json new file mode 100644 index 0000000000..fb6ccb2d18 --- /dev/null +++ b/services/github/github-resources/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types" + } +} \ No newline at end of file diff --git a/services/github/github.graphql b/services/github/github.graphql new file mode 100644 index 0000000000..b95c8eb539 --- /dev/null +++ b/services/github/github.graphql @@ -0,0 +1,59647 @@ +directive @requiredCapabilities( + requiredCapabilities: [String!] +) on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION + +""" +Marks an element of a GraphQL schema as only available via a preview header +""" +directive @preview( + """ + The identifier of the API preview that toggles this field. + """ + toggledBy: String! +) on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION + +""" +Defines what type of global IDs are accepted for a mutation argument of type ID. +""" +directive @possibleTypes( + """ + Abstract type of accepted global ID + """ + abstractType: String + + """ + Accepted types of global IDs. + """ + concreteTypes: [String!]! +) on INPUT_FIELD_DEFINITION + +""" +Autogenerated input type of AbortQueuedMigrations +""" +input AbortQueuedMigrationsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the organization that is running the migrations. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of AbortQueuedMigrations +""" +type AbortQueuedMigrationsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Did the operation succeed? + """ + success: Boolean +} + +""" +Autogenerated input type of AcceptEnterpriseAdministratorInvitation +""" +input AcceptEnterpriseAdministratorInvitationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the invitation being accepted + """ + invitationId: ID! @possibleTypes(concreteTypes: ["EnterpriseAdministratorInvitation"]) +} + +""" +Autogenerated return type of AcceptEnterpriseAdministratorInvitation +""" +type AcceptEnterpriseAdministratorInvitationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The invitation that was accepted. + """ + invitation: EnterpriseAdministratorInvitation + + """ + A message confirming the result of accepting an administrator invitation. + """ + message: String +} + +""" +Autogenerated input type of AcceptTopicSuggestion +""" +input AcceptTopicSuggestionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the suggested topic. + """ + name: String! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of AcceptTopicSuggestion +""" +type AcceptTopicSuggestionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The accepted topic. + """ + topic: Topic +} + +""" +Represents an object which can take actions on GitHub. Typically a User or Bot. +""" +interface Actor { + """ + A URL pointing to the actor's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The username of the actor. + """ + login: String! + + """ + The HTTP path for this actor. + """ + resourcePath: URI! + + """ + The HTTP URL for this actor. + """ + url: URI! +} + +""" +Location information for an actor +""" +type ActorLocation { + """ + City + """ + city: String + + """ + Country name + """ + country: String + + """ + Country code + """ + countryCode: String + + """ + Region name + """ + region: String + + """ + Region or state code + """ + regionCode: String +} + +""" +The actor's type. +""" +enum ActorType { + """ + Indicates a team actor. + """ + TEAM + + """ + Indicates a user actor. + """ + USER +} + +""" +Autogenerated input type of AddAssigneesToAssignable +""" +input AddAssigneesToAssignableInput { + """ + The id of the assignable object to add assignees to. + """ + assignableId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "Assignable") + + """ + The id of users to add as assignees. + """ + assigneeIds: [ID!]! @possibleTypes(concreteTypes: ["User"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of AddAssigneesToAssignable +""" +type AddAssigneesToAssignablePayload { + """ + The item that was assigned. + """ + assignable: Assignable + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of AddComment +""" +input AddCommentInput { + """ + The contents of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "IssueOrPullRequest") +} + +""" +Autogenerated return type of AddComment +""" +type AddCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The edge from the subject's comment connection. + """ + commentEdge: IssueCommentEdge + + """ + The subject + """ + subject: Node + + """ + The edge from the subject's timeline connection. + """ + timelineEdge: IssueTimelineItemEdge +} + +""" +Autogenerated input type of AddDiscussionComment +""" +input AddDiscussionCommentInput { + """ + The contents of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion to comment on. + """ + discussionId: ID! @possibleTypes(concreteTypes: ["Discussion"]) + + """ + The Node ID of the discussion comment within this discussion to reply to. + """ + replyToId: ID @possibleTypes(concreteTypes: ["DiscussionComment"]) +} + +""" +Autogenerated return type of AddDiscussionComment +""" +type AddDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created discussion comment. + """ + comment: DiscussionComment +} + +""" +Autogenerated input type of AddDiscussionPollVote +""" +input AddDiscussionPollVoteInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion poll option to vote for. + """ + pollOptionId: ID! @possibleTypes(concreteTypes: ["DiscussionPollOption"]) +} + +""" +Autogenerated return type of AddDiscussionPollVote +""" +type AddDiscussionPollVotePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The poll option that a vote was added to. + """ + pollOption: DiscussionPollOption +} + +""" +Autogenerated input type of AddEnterpriseOrganizationMember +""" +input AddEnterpriseOrganizationMemberInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise which owns the organization. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The ID of the organization the users will be added to. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + The role to assign the users in the organization + """ + role: OrganizationMemberRole + + """ + The IDs of the enterprise members to add. + """ + userIds: [ID!]! +} + +""" +Autogenerated return type of AddEnterpriseOrganizationMember +""" +type AddEnterpriseOrganizationMemberPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The users who were added to the organization. + """ + users: [User!] +} + +""" +Autogenerated input type of AddEnterpriseSupportEntitlement +""" +input AddEnterpriseSupportEntitlementInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Enterprise which the admin belongs to. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a member who will receive the support entitlement. + """ + login: String! +} + +""" +Autogenerated return type of AddEnterpriseSupportEntitlement +""" +type AddEnterpriseSupportEntitlementPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of adding the support entitlement. + """ + message: String +} + +""" +Autogenerated input type of AddLabelsToLabelable +""" +input AddLabelsToLabelableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ids of the labels to add. + """ + labelIds: [ID!]! @possibleTypes(concreteTypes: ["Label"]) + + """ + The id of the labelable object to add labels to. + """ + labelableId: ID! @possibleTypes(concreteTypes: ["Discussion", "Issue", "PullRequest"], abstractType: "Labelable") +} + +""" +Autogenerated return type of AddLabelsToLabelable +""" +type AddLabelsToLabelablePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was labeled. + """ + labelable: Labelable +} + +""" +Autogenerated input type of AddProjectCard +""" +input AddProjectCardInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The content of the card. Must be a member of the ProjectCardItem union + """ + contentId: ID @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "ProjectCardItem") + + """ + The note on the card. + """ + note: String + + """ + The Node ID of the ProjectColumn. + """ + projectColumnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of AddProjectCard +""" +type AddProjectCardPayload { + """ + The edge from the ProjectColumn's card connection. + """ + cardEdge: ProjectCardEdge + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ProjectColumn + """ + projectColumn: ProjectColumn +} + +""" +Autogenerated input type of AddProjectColumn +""" +input AddProjectColumnInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the column. + """ + name: String! + + """ + The Node ID of the project. + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) +} + +""" +Autogenerated return type of AddProjectColumn +""" +type AddProjectColumnPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The edge from the project's column connection. + """ + columnEdge: ProjectColumnEdge + + """ + The project + """ + project: Project +} + +""" +Autogenerated input type of AddProjectV2DraftIssue +""" +input AddProjectV2DraftIssueInput { + """ + The IDs of the assignees of the draft issue. + """ + assigneeIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The body of the draft issue. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project to add the draft issue to. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The title of the draft issue. A project item can also be created by providing + the URL of an Issue or Pull Request if you have access. + """ + title: String! +} + +""" +Autogenerated return type of AddProjectV2DraftIssue +""" +type AddProjectV2DraftIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The draft issue added to the project. + """ + projectItem: ProjectV2Item +} + +""" +Autogenerated input type of AddProjectV2ItemById +""" +input AddProjectV2ItemByIdInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the Issue or Pull Request to add. + """ + contentId: ID! + @possibleTypes(concreteTypes: ["DraftIssue", "Issue", "PullRequest"], abstractType: "ProjectV2ItemContent") + + """ + The ID of the Project to add the item to. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of AddProjectV2ItemById +""" +type AddProjectV2ItemByIdPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item added to the project. + """ + item: ProjectV2Item +} + +""" +Autogenerated input type of AddPullRequestReviewComment +""" +input AddPullRequestReviewCommentInput { + """ + The text of the comment. This field is required + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `body` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The SHA of the commit to comment on. + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `commitOID` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + commitOID: GitObjectID + + """ + The comment id to reply to. + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `inReplyTo` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + inReplyTo: ID @possibleTypes(concreteTypes: ["PullRequestReviewComment"]) + + """ + The relative path of the file to comment on. + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `path` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + path: String + + """ + The line index in the diff to comment on. + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `position` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + position: Int + + """ + The node ID of the pull request reviewing + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `pullRequestId` will be removed. use + addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + pullRequestId: ID @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The Node ID of the review to modify. + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `pullRequestReviewId` will be removed. use + addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + pullRequestReviewId: ID @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of AddPullRequestReviewComment +""" +type AddPullRequestReviewCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created comment. + """ + comment: PullRequestReviewComment + + """ + The edge from the review's comment connection. + """ + commentEdge: PullRequestReviewCommentEdge +} + +""" +Autogenerated input type of AddPullRequestReview +""" +input AddPullRequestReviewInput { + """ + The contents of the review body comment. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The review line comments. + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `comments` will be removed. use the `threads` argument instead + **Reason:** We are deprecating comment fields that use diff-relative positioning + """ + comments: [DraftPullRequestReviewComment] + + """ + The commit OID the review pertains to. + """ + commitOID: GitObjectID + + """ + The event to perform on the pull request review. + """ + event: PullRequestReviewEvent + + """ + The Node ID of the pull request to modify. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The review line comment threads. + """ + threads: [DraftPullRequestReviewThread] +} + +""" +Autogenerated return type of AddPullRequestReview +""" +type AddPullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created pull request review. + """ + pullRequestReview: PullRequestReview + + """ + The edge from the pull request's review connection. + """ + reviewEdge: PullRequestReviewEdge +} + +""" +Autogenerated input type of AddPullRequestReviewThread +""" +input AddPullRequestReviewThreadInput { + """ + Body of the thread's first comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The line of the blob to which the thread refers, required for line-level + threads. The end of the line range for multi-line comments. + """ + line: Int + + """ + Path to the file being commented on. + """ + path: String! + + """ + The node ID of the pull request reviewing + """ + pullRequestId: ID @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The Node ID of the review to modify. + """ + pullRequestReviewId: ID @possibleTypes(concreteTypes: ["PullRequestReview"]) + + """ + The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range. + """ + side: DiffSide = RIGHT + + """ + The first line of the range to which the comment refers. + """ + startLine: Int + + """ + The side of the diff on which the start line resides. + """ + startSide: DiffSide = RIGHT + + """ + The level at which the comments in the corresponding thread are targeted, can be a diff line or a file + """ + subjectType: PullRequestReviewThreadSubjectType = LINE +} + +""" +Autogenerated return type of AddPullRequestReviewThread +""" +type AddPullRequestReviewThreadPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created thread. + """ + thread: PullRequestReviewThread +} + +""" +Autogenerated input type of AddPullRequestReviewThreadReply +""" +input AddPullRequestReviewThreadReplyInput { + """ + The text of the reply. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pending review to which the reply will belong. + """ + pullRequestReviewId: ID @possibleTypes(concreteTypes: ["PullRequestReview"]) + + """ + The Node ID of the thread to which this reply is being written. + """ + pullRequestReviewThreadId: ID! @possibleTypes(concreteTypes: ["PullRequestReviewThread"]) +} + +""" +Autogenerated return type of AddPullRequestReviewThreadReply +""" +type AddPullRequestReviewThreadReplyPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created reply. + """ + comment: PullRequestReviewComment +} + +""" +Autogenerated input type of AddReaction +""" +input AddReactionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the emoji to react with. + """ + content: ReactionContent! + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! + @possibleTypes( + concreteTypes: [ + "CommitComment" + "Discussion" + "DiscussionComment" + "Issue" + "IssueComment" + "PullRequest" + "PullRequestReview" + "PullRequestReviewComment" + "Release" + "TeamDiscussion" + "TeamDiscussionComment" + ] + abstractType: "Reactable" + ) +} + +""" +Autogenerated return type of AddReaction +""" +type AddReactionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The reaction object. + """ + reaction: Reaction + + """ + The reaction groups for the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + The reactable subject. + """ + subject: Reactable +} + +""" +Autogenerated input type of AddStar +""" +input AddStarInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Starrable ID to star. + """ + starrableId: ID! @possibleTypes(concreteTypes: ["Gist", "Repository", "Topic"], abstractType: "Starrable") +} + +""" +Autogenerated return type of AddStar +""" +type AddStarPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The starrable. + """ + starrable: Starrable +} + +""" +Autogenerated input type of AddUpvote +""" +input AddUpvoteInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion or comment to upvote. + """ + subjectId: ID! @possibleTypes(concreteTypes: ["Discussion", "DiscussionComment"], abstractType: "Votable") +} + +""" +Autogenerated return type of AddUpvote +""" +type AddUpvotePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The votable subject. + """ + subject: Votable +} + +""" +Autogenerated input type of AddVerifiableDomain +""" +input AddVerifiableDomainInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The URL of the domain + """ + domain: URI! + + """ + The ID of the owner to add the domain to + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Enterprise", "Organization"], abstractType: "VerifiableDomainOwner") +} + +""" +Autogenerated return type of AddVerifiableDomain +""" +type AddVerifiableDomainPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The verifiable domain that was added. + """ + domain: VerifiableDomain +} + +""" +Represents an 'added_to_merge_queue' event on a given pull request. +""" +type AddedToMergeQueueEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who added this Pull Request to the merge queue + """ + enqueuer: User + id: ID! + + """ + The merge queue where this pull request was added to. + """ + mergeQueue: MergeQueue + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents a 'added_to_project' event on a given issue or pull request. +""" +type AddedToProjectEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Project referenced by event. + """ + project: Project @preview(toggledBy: "starfox-preview") + + """ + Project card referenced by this project event. + """ + projectCard: ProjectCard @preview(toggledBy: "starfox-preview") + + """ + Column name referenced by this project event. + """ + projectColumnName: String! @preview(toggledBy: "starfox-preview") +} + +""" +Represents an announcement banner. +""" +interface AnnouncementBanner { + """ + The text of the announcement + """ + announcement: String + + """ + The expiration date of the announcement, if any + """ + announcementExpiresAt: DateTime + + """ + Whether the announcement can be dismissed by the user + """ + announcementUserDismissible: Boolean +} + +""" +A GitHub App. +""" +type App implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The description of the app. + """ + description: String + id: ID! + + """ + The IP addresses of the app. + """ + ipAllowListEntries( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for IP allow list entries returned. + """ + orderBy: IpAllowListEntryOrder = {field: ALLOW_LIST_VALUE, direction: ASC} + ): IpAllowListEntryConnection! + + """ + The hex color code, without the leading '#', for the logo background. + """ + logoBackgroundColor: String! + + """ + A URL pointing to the app's logo. + """ + logoUrl( + """ + The size of the resulting image. + """ + size: Int + ): URI! + + """ + The name of the app. + """ + name: String! + + """ + A slug based on the name of the app for use in URLs. + """ + slug: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The URL to the app's homepage. + """ + url: URI! +} + +""" +Autogenerated input type of ApproveDeployments +""" +input ApproveDeploymentsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Optional comment for approving deployments + """ + comment: String = "" + + """ + The ids of environments to reject deployments + """ + environmentIds: [ID!]! + + """ + The node ID of the workflow run containing the pending deployments. + """ + workflowRunId: ID! @possibleTypes(concreteTypes: ["WorkflowRun"]) +} + +""" +Autogenerated return type of ApproveDeployments +""" +type ApproveDeploymentsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The affected deployments. + """ + deployments: [Deployment!] +} + +""" +Autogenerated input type of ApproveVerifiableDomain +""" +input ApproveVerifiableDomainInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the verifiable domain to approve. + """ + id: ID! @possibleTypes(concreteTypes: ["VerifiableDomain"]) +} + +""" +Autogenerated return type of ApproveVerifiableDomain +""" +type ApproveVerifiableDomainPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The verifiable domain that was approved. + """ + domain: VerifiableDomain +} + +""" +Autogenerated input type of ArchiveProjectV2Item +""" +input ArchiveProjectV2ItemInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the ProjectV2Item to archive. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the Project to archive the item from. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of ArchiveProjectV2Item +""" +type ArchiveProjectV2ItemPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item archived from the project. + """ + item: ProjectV2Item +} + +""" +Autogenerated input type of ArchiveRepository +""" +input ArchiveRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the repository to mark as archived. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of ArchiveRepository +""" +type ArchiveRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository that was marked as archived. + """ + repository: Repository +} + +""" +An object that can have users assigned to it. +""" +interface Assignable { + """ + A list of Users assigned to this object. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! +} + +""" +Represents an 'assigned' event on any assignable object. +""" +type AssignedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the assignable associated with the event. + """ + assignable: Assignable! + + """ + Identifies the user or mannequin that was assigned. + """ + assignee: Assignee + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the user who was assigned. + """ + user: User + @deprecated(reason: "Assignees can now be mannequins. Use the `assignee` field instead. Removal on 2020-01-01 UTC.") +} + +""" +Types that can be assigned to issues. +""" +union Assignee = Bot | Mannequin | Organization | User + +""" +An entry in the audit log. +""" +interface AuditEntry { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Types that can initiate an audit log event. +""" +union AuditEntryActor = Bot | Organization | User + +""" +Ordering options for Audit Log connections. +""" +input AuditLogOrder { + """ + The ordering direction. + """ + direction: OrderDirection + + """ + The field to order Audit Logs by. + """ + field: AuditLogOrderField +} + +""" +Properties by which Audit Log connections can be ordered. +""" +enum AuditLogOrderField { + """ + Order audit log entries by timestamp + """ + CREATED_AT +} + +""" +Represents a 'auto_merge_disabled' event on a given pull request. +""" +type AutoMergeDisabledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who disabled auto-merge for this Pull Request + """ + disabler: User + id: ID! + + """ + PullRequest referenced by event + """ + pullRequest: PullRequest + + """ + The reason auto-merge was disabled + """ + reason: String + + """ + The reason_code relating to why auto-merge was disabled + """ + reasonCode: String +} + +""" +Represents a 'auto_merge_enabled' event on a given pull request. +""" +type AutoMergeEnabledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who enabled auto-merge for this Pull Request + """ + enabler: User + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents an auto-merge request for a pull request +""" +type AutoMergeRequest { + """ + The email address of the author of this auto-merge request. + """ + authorEmail: String + + """ + The commit message of the auto-merge request. If a merge queue is required by + the base branch, this value will be set by the merge queue when merging. + """ + commitBody: String + + """ + The commit title of the auto-merge request. If a merge queue is required by + the base branch, this value will be set by the merge queue when merging + """ + commitHeadline: String + + """ + When was this auto-merge request was enabled. + """ + enabledAt: DateTime + + """ + The actor who created the auto-merge request. + """ + enabledBy: Actor + + """ + The merge method of the auto-merge request. If a merge queue is required by + the base branch, this value will be set by the merge queue when merging. + """ + mergeMethod: PullRequestMergeMethod! + + """ + The pull request that this auto-merge request is set against. + """ + pullRequest: PullRequest! +} + +""" +Represents a 'auto_rebase_enabled' event on a given pull request. +""" +type AutoRebaseEnabledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who enabled auto-merge (rebase) for this Pull Request + """ + enabler: User + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents a 'auto_squash_enabled' event on a given pull request. +""" +type AutoSquashEnabledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who enabled auto-merge (squash) for this Pull Request + """ + enabler: User + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents a 'automatic_base_change_failed' event on a given pull request. +""" +type AutomaticBaseChangeFailedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + The new base for this PR + """ + newBase: String! + + """ + The old base for this PR + """ + oldBase: String! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Represents a 'automatic_base_change_succeeded' event on a given pull request. +""" +type AutomaticBaseChangeSucceededEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + The new base for this PR + """ + newBase: String! + + """ + The old base for this PR + """ + oldBase: String! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +A (potentially binary) string encoded using base64. +""" +scalar Base64String + +""" +Represents a 'base_ref_changed' event on a given issue or pull request. +""" +type BaseRefChangedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the name of the base ref for the pull request after it was changed. + """ + currentRefName: String! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Identifies the name of the base ref for the pull request before it was changed. + """ + previousRefName: String! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Represents a 'base_ref_deleted' event on a given pull request. +""" +type BaseRefDeletedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the name of the Ref associated with the `base_ref_deleted` event. + """ + baseRefName: String + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents a 'base_ref_force_pushed' event on a given pull request. +""" +type BaseRefForcePushedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the after commit SHA for the 'base_ref_force_pushed' event. + """ + afterCommit: Commit + + """ + Identifies the before commit SHA for the 'base_ref_force_pushed' event. + """ + beforeCommit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the fully qualified ref name for the 'base_ref_force_pushed' event. + """ + ref: Ref +} + +""" +Represents non-fractional signed whole numeric values. Since the value may +exceed the size of a 32-bit integer, it's encoded as a string. +""" +scalar BigInt + +""" +Represents a Git blame. +""" +type Blame { + """ + The list of ranges from a Git blame. + """ + ranges: [BlameRange!]! +} + +""" +Represents a range of information from a Git blame. +""" +type BlameRange { + """ + Identifies the recency of the change, from 1 (new) to 10 (old). This is + calculated as a 2-quantile and determines the length of distance between the + median age of all the changes in the file and the recency of the current + range's change. + """ + age: Int! + + """ + Identifies the line author + """ + commit: Commit! + + """ + The ending line for the range + """ + endingLine: Int! + + """ + The starting line for the range + """ + startingLine: Int! +} + +""" +Represents a Git blob. +""" +type Blob implements GitObject & Node { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + Byte size of Blob object + """ + byteSize: Int! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + id: ID! + + """ + Indicates whether the Blob is binary or text. Returns null if unable to determine the encoding. + """ + isBinary: Boolean + + """ + Indicates whether the contents is truncated + """ + isTruncated: Boolean! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The Repository the Git object belongs to + """ + repository: Repository! + + """ + UTF8 text data or null if the Blob is binary + """ + text: String +} + +""" +A special type of user which takes actions on behalf of GitHub Apps. +""" +type Bot implements Actor & Node & UniformResourceLocatable { + """ + A URL pointing to the GitHub App's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The username of the actor. + """ + login: String! + + """ + The HTTP path for this bot + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this bot + """ + url: URI! +} + +""" +Types which can be actors for `BranchActorAllowance` objects. +""" +union BranchActorAllowanceActor = App | Team | User + +""" +Parameters to be used for the branch_name_pattern rule +""" +type BranchNamePatternParameters { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean! + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Parameters to be used for the branch_name_pattern rule +""" +input BranchNamePatternParametersInput { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +A branch protection rule. +""" +type BranchProtectionRule implements Node { + """ + Can this branch be deleted. + """ + allowsDeletions: Boolean! + + """ + Are force pushes allowed on this branch. + """ + allowsForcePushes: Boolean! + + """ + Is branch creation a protected operation. + """ + blocksCreations: Boolean! + + """ + A list of conflicts matching branches protection rule and other branch protection rules + """ + branchProtectionRuleConflicts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): BranchProtectionRuleConflictConnection! + + """ + A list of actors able to force push for this branch protection rule. + """ + bypassForcePushAllowances( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): BypassForcePushAllowanceConnection! + + """ + A list of actors able to bypass PRs for this branch protection rule. + """ + bypassPullRequestAllowances( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): BypassPullRequestAllowanceConnection! + + """ + The actor who created this branch protection rule. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + Will new commits pushed to matching branches dismiss pull request review approvals. + """ + dismissesStaleReviews: Boolean! + id: ID! + + """ + Can admins overwrite branch protection. + """ + isAdminEnforced: Boolean! + + """ + Whether users can pull changes from upstream when the branch is locked. Set to + `true` to allow fork syncing. Set to `false` to prevent fork syncing. + """ + lockAllowsFetchAndMerge: Boolean! + + """ + Whether to set the branch as read-only. If this is true, users will not be able to push to the branch. + """ + lockBranch: Boolean! + + """ + Repository refs that are protected by this rule + """ + matchingRefs( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filters refs with query on name + """ + query: String + ): RefConnection! + + """ + Identifies the protection rule pattern. + """ + pattern: String! + + """ + A list push allowances for this branch protection rule. + """ + pushAllowances( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PushAllowanceConnection! + + """ + The repository associated with this branch protection rule. + """ + repository: Repository + + """ + Whether the most recent push must be approved by someone other than the person who pushed it + """ + requireLastPushApproval: Boolean! + + """ + Number of approving reviews required to update matching branches. + """ + requiredApprovingReviewCount: Int + + """ + List of required deployment environments that must be deployed successfully to update matching branches + """ + requiredDeploymentEnvironments: [String] + + """ + List of required status check contexts that must pass for commits to be accepted to matching branches. + """ + requiredStatusCheckContexts: [String] + + """ + List of required status checks that must pass for commits to be accepted to matching branches. + """ + requiredStatusChecks: [RequiredStatusCheckDescription!] + + """ + Are approving reviews required to update matching branches. + """ + requiresApprovingReviews: Boolean! + + """ + Are reviews from code owners required to update matching branches. + """ + requiresCodeOwnerReviews: Boolean! + + """ + Are commits required to be signed. + """ + requiresCommitSignatures: Boolean! + + """ + Are conversations required to be resolved before merging. + """ + requiresConversationResolution: Boolean! + + """ + Does this branch require deployment to specific environments before merging + """ + requiresDeployments: Boolean! + + """ + Are merge commits prohibited from being pushed to this branch. + """ + requiresLinearHistory: Boolean! + + """ + Are status checks required to update matching branches. + """ + requiresStatusChecks: Boolean! + + """ + Are branches required to be up to date before merging. + """ + requiresStrictStatusChecks: Boolean! + + """ + Is pushing to matching branches restricted. + """ + restrictsPushes: Boolean! + + """ + Is dismissal of pull request reviews restricted. + """ + restrictsReviewDismissals: Boolean! + + """ + A list review dismissal allowances for this branch protection rule. + """ + reviewDismissalAllowances( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ReviewDismissalAllowanceConnection! +} + +""" +A conflict between two branch protection rules. +""" +type BranchProtectionRuleConflict { + """ + Identifies the branch protection rule. + """ + branchProtectionRule: BranchProtectionRule + + """ + Identifies the conflicting branch protection rule. + """ + conflictingBranchProtectionRule: BranchProtectionRule + + """ + Identifies the branch ref that has conflicting rules + """ + ref: Ref +} + +""" +The connection type for BranchProtectionRuleConflict. +""" +type BranchProtectionRuleConflictConnection { + """ + A list of edges. + """ + edges: [BranchProtectionRuleConflictEdge] + + """ + A list of nodes. + """ + nodes: [BranchProtectionRuleConflict] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type BranchProtectionRuleConflictEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: BranchProtectionRuleConflict +} + +""" +The connection type for BranchProtectionRule. +""" +type BranchProtectionRuleConnection { + """ + A list of edges. + """ + edges: [BranchProtectionRuleEdge] + + """ + A list of nodes. + """ + nodes: [BranchProtectionRule] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type BranchProtectionRuleEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: BranchProtectionRule +} + +""" +Information about a sponsorship to make for a user or organization with a GitHub +Sponsors profile, as part of sponsoring many users or organizations at once. +""" +input BulkSponsorship { + """ + The amount to pay to the sponsorable in US dollars. Valid values: 1-12000. + """ + amount: Int! + + """ + The ID of the user or organization who is receiving the sponsorship. Required if sponsorableLogin is not given. + """ + sponsorableId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsorable") + + """ + The username of the user or organization who is receiving the sponsorship. Required if sponsorableId is not given. + """ + sponsorableLogin: String +} + +""" +Types that can represent a repository ruleset bypass actor. +""" +union BypassActor = App | Team + +""" +A user, team, or app who has the ability to bypass a force push requirement on a protected branch. +""" +type BypassForcePushAllowance implements Node { + """ + The actor that can force push. + """ + actor: BranchActorAllowanceActor + + """ + Identifies the branch protection rule associated with the allowed user, team, or app. + """ + branchProtectionRule: BranchProtectionRule + id: ID! +} + +""" +The connection type for BypassForcePushAllowance. +""" +type BypassForcePushAllowanceConnection { + """ + A list of edges. + """ + edges: [BypassForcePushAllowanceEdge] + + """ + A list of nodes. + """ + nodes: [BypassForcePushAllowance] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type BypassForcePushAllowanceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: BypassForcePushAllowance +} + +""" +A user, team, or app who has the ability to bypass a pull request requirement on a protected branch. +""" +type BypassPullRequestAllowance implements Node { + """ + The actor that can bypass. + """ + actor: BranchActorAllowanceActor + + """ + Identifies the branch protection rule associated with the allowed user, team, or app. + """ + branchProtectionRule: BranchProtectionRule + id: ID! +} + +""" +The connection type for BypassPullRequestAllowance. +""" +type BypassPullRequestAllowanceConnection { + """ + A list of edges. + """ + edges: [BypassPullRequestAllowanceEdge] + + """ + A list of nodes. + """ + nodes: [BypassPullRequestAllowance] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type BypassPullRequestAllowanceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: BypassPullRequestAllowance +} + +""" +The Common Vulnerability Scoring System +""" +type CVSS { + """ + The CVSS score associated with this advisory + """ + score: Float! + + """ + The CVSS vector string associated with this advisory + """ + vectorString: String +} + +""" +A common weakness enumeration +""" +type CWE implements Node { + """ + The id of the CWE + """ + cweId: String! + + """ + A detailed description of this CWE + """ + description: String! + id: ID! + + """ + The name of this CWE + """ + name: String! +} + +""" +The connection type for CWE. +""" +type CWEConnection { + """ + A list of edges. + """ + edges: [CWEEdge] + + """ + A list of nodes. + """ + nodes: [CWE] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CWEEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CWE +} + +""" +Autogenerated input type of CancelEnterpriseAdminInvitation +""" +input CancelEnterpriseAdminInvitationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pending enterprise administrator invitation. + """ + invitationId: ID! @possibleTypes(concreteTypes: ["EnterpriseAdministratorInvitation"]) +} + +""" +Autogenerated return type of CancelEnterpriseAdminInvitation +""" +type CancelEnterpriseAdminInvitationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The invitation that was canceled. + """ + invitation: EnterpriseAdministratorInvitation + + """ + A message confirming the result of canceling an administrator invitation. + """ + message: String +} + +""" +Autogenerated input type of CancelSponsorship +""" +input CancelSponsorshipInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the user or organization who is acting as the sponsor, paying for + the sponsorship. Required if sponsorLogin is not given. + """ + sponsorId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsor") + + """ + The username of the user or organization who is acting as the sponsor, paying + for the sponsorship. Required if sponsorId is not given. + """ + sponsorLogin: String + + """ + The ID of the user or organization who is receiving the sponsorship. Required if sponsorableLogin is not given. + """ + sponsorableId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsorable") + + """ + The username of the user or organization who is receiving the sponsorship. Required if sponsorableId is not given. + """ + sponsorableLogin: String +} + +""" +Autogenerated return type of CancelSponsorship +""" +type CancelSponsorshipPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The tier that was being used at the time of cancellation. + """ + sponsorsTier: SponsorsTier +} + +""" +Autogenerated input type of ChangeUserStatus +""" +input ChangeUserStatusInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The emoji to represent your status. Can either be a native Unicode emoji or an emoji name with colons, e.g., :grinning:. + """ + emoji: String + + """ + If set, the user status will not be shown after this date. + """ + expiresAt: DateTime + + """ + Whether this status should indicate you are not fully available on GitHub, e.g., you are away. + """ + limitedAvailability: Boolean = false + + """ + A short description of your current status. + """ + message: String + + """ + The ID of the organization whose members will be allowed to see the status. If + omitted, the status will be publicly visible. + """ + organizationId: ID @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of ChangeUserStatus +""" +type ChangeUserStatusPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Your updated status. + """ + status: UserStatus +} + +""" +A single check annotation. +""" +type CheckAnnotation { + """ + The annotation's severity level. + """ + annotationLevel: CheckAnnotationLevel + + """ + The path to the file that this annotation was made on. + """ + blobUrl: URI! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The position of this annotation. + """ + location: CheckAnnotationSpan! + + """ + The annotation's message. + """ + message: String! + + """ + The path that this annotation was made on. + """ + path: String! + + """ + Additional information about the annotation. + """ + rawDetails: String + + """ + The annotation's title + """ + title: String +} + +""" +The connection type for CheckAnnotation. +""" +type CheckAnnotationConnection { + """ + A list of edges. + """ + edges: [CheckAnnotationEdge] + + """ + A list of nodes. + """ + nodes: [CheckAnnotation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Information from a check run analysis to specific lines of code. +""" +input CheckAnnotationData { + """ + Represents an annotation's information level + """ + annotationLevel: CheckAnnotationLevel! + + """ + The location of the annotation + """ + location: CheckAnnotationRange! + + """ + A short description of the feedback for these lines of code. + """ + message: String! + + """ + The path of the file to add an annotation to. + """ + path: String! + + """ + Details about this annotation. + """ + rawDetails: String + + """ + The title that represents the annotation. + """ + title: String +} + +""" +An edge in a connection. +""" +type CheckAnnotationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CheckAnnotation +} + +""" +Represents an annotation's information level. +""" +enum CheckAnnotationLevel { + """ + An annotation indicating an inescapable error. + """ + FAILURE + + """ + An annotation indicating some information. + """ + NOTICE + + """ + An annotation indicating an ignorable error. + """ + WARNING +} + +""" +A character position in a check annotation. +""" +type CheckAnnotationPosition { + """ + Column number (1 indexed). + """ + column: Int + + """ + Line number (1 indexed). + """ + line: Int! +} + +""" +Information from a check run analysis to specific lines of code. +""" +input CheckAnnotationRange { + """ + The ending column of the range. + """ + endColumn: Int + + """ + The ending line of the range. + """ + endLine: Int! + + """ + The starting column of the range. + """ + startColumn: Int + + """ + The starting line of the range. + """ + startLine: Int! +} + +""" +An inclusive pair of positions for a check annotation. +""" +type CheckAnnotationSpan { + """ + End position (inclusive). + """ + end: CheckAnnotationPosition! + + """ + Start position (inclusive). + """ + start: CheckAnnotationPosition! +} + +""" +The possible states for a check suite or run conclusion. +""" +enum CheckConclusionState { + """ + The check suite or run requires action. + """ + ACTION_REQUIRED + + """ + The check suite or run has been cancelled. + """ + CANCELLED + + """ + The check suite or run has failed. + """ + FAILURE + + """ + The check suite or run was neutral. + """ + NEUTRAL + + """ + The check suite or run was skipped. + """ + SKIPPED + + """ + The check suite or run was marked stale by GitHub. Only GitHub can use this conclusion. + """ + STALE + + """ + The check suite or run has failed at startup. + """ + STARTUP_FAILURE + + """ + The check suite or run has succeeded. + """ + SUCCESS + + """ + The check suite or run has timed out. + """ + TIMED_OUT +} + +""" +A check run. +""" +type CheckRun implements Node & RequirableByPullRequest & UniformResourceLocatable { + """ + The check run's annotations + """ + annotations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CheckAnnotationConnection + + """ + The check suite that this run is a part of. + """ + checkSuite: CheckSuite! + + """ + Identifies the date and time when the check run was completed. + """ + completedAt: DateTime + + """ + The conclusion of the check run. + """ + conclusion: CheckConclusionState + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The corresponding deployment for this job, if any + """ + deployment: Deployment + + """ + The URL from which to find full details of the check run on the integrator's site. + """ + detailsUrl: URI + + """ + A reference for the check run on the integrator's system. + """ + externalId: String + id: ID! + + """ + Whether this is required to pass before merging for a specific pull request. + """ + isRequired( + """ + The id of the pull request this is required for + """ + pullRequestId: ID + + """ + The number of the pull request this is required for + """ + pullRequestNumber: Int + ): Boolean! + + """ + The name of the check for this check run. + """ + name: String! + + """ + Information about a pending deployment, if any, in this check run + """ + pendingDeploymentRequest: DeploymentRequest + + """ + The permalink to the check run summary. + """ + permalink: URI! + + """ + The repository associated with this check run. + """ + repository: Repository! + + """ + The HTTP path for this check run. + """ + resourcePath: URI! + + """ + Identifies the date and time when the check run was started. + """ + startedAt: DateTime + + """ + The current status of the check run. + """ + status: CheckStatusState! + + """ + The check run's steps + """ + steps( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Step number + """ + number: Int + ): CheckStepConnection + + """ + A string representing the check run's summary + """ + summary: String + + """ + A string representing the check run's text + """ + text: String + + """ + A string representing the check run + """ + title: String + + """ + The HTTP URL for this check run. + """ + url: URI! +} + +""" +Possible further actions the integrator can perform. +""" +input CheckRunAction { + """ + A short explanation of what this action would do. + """ + description: String! + + """ + A reference for the action on the integrator's system. + """ + identifier: String! + + """ + The text to be displayed on a button in the web UI. + """ + label: String! +} + +""" +The connection type for CheckRun. +""" +type CheckRunConnection { + """ + A list of edges. + """ + edges: [CheckRunEdge] + + """ + A list of nodes. + """ + nodes: [CheckRun] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CheckRunEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CheckRun +} + +""" +The filters that are available when fetching check runs. +""" +input CheckRunFilter { + """ + Filters the check runs created by this application ID. + """ + appId: Int + + """ + Filters the check runs by this name. + """ + checkName: String + + """ + Filters the check runs by this type. + """ + checkType: CheckRunType + + """ + Filters the check runs by these conclusions. + """ + conclusions: [CheckConclusionState!] + + """ + Filters the check runs by this status. Superceded by statuses. + """ + status: CheckStatusState + + """ + Filters the check runs by this status. Overrides status. + """ + statuses: [CheckStatusState!] +} + +""" +Descriptive details about the check run. +""" +input CheckRunOutput { + """ + The annotations that are made as part of the check run. + """ + annotations: [CheckAnnotationData!] + + """ + Images attached to the check run output displayed in the GitHub pull request UI. + """ + images: [CheckRunOutputImage!] + + """ + The summary of the check run (supports Commonmark). + """ + summary: String! + + """ + The details of the check run (supports Commonmark). + """ + text: String + + """ + A title to provide for this check run. + """ + title: String! +} + +""" +Images attached to the check run output displayed in the GitHub pull request UI. +""" +input CheckRunOutputImage { + """ + The alternative text for the image. + """ + alt: String! + + """ + A short image description. + """ + caption: String + + """ + The full URL of the image. + """ + imageUrl: URI! +} + +""" +The possible states of a check run in a status rollup. +""" +enum CheckRunState { + """ + The check run requires action. + """ + ACTION_REQUIRED + + """ + The check run has been cancelled. + """ + CANCELLED + + """ + The check run has been completed. + """ + COMPLETED + + """ + The check run has failed. + """ + FAILURE + + """ + The check run is in progress. + """ + IN_PROGRESS + + """ + The check run was neutral. + """ + NEUTRAL + + """ + The check run is in pending state. + """ + PENDING + + """ + The check run has been queued. + """ + QUEUED + + """ + The check run was skipped. + """ + SKIPPED + + """ + The check run was marked stale by GitHub. Only GitHub can use this conclusion. + """ + STALE + + """ + The check run has failed at startup. + """ + STARTUP_FAILURE + + """ + The check run has succeeded. + """ + SUCCESS + + """ + The check run has timed out. + """ + TIMED_OUT + + """ + The check run is in waiting state. + """ + WAITING +} + +""" +Represents a count of the state of a check run. +""" +type CheckRunStateCount { + """ + The number of check runs with this state. + """ + count: Int! + + """ + The state of a check run. + """ + state: CheckRunState! +} + +""" +The possible types of check runs. +""" +enum CheckRunType { + """ + Every check run available. + """ + ALL + + """ + The latest check run. + """ + LATEST +} + +""" +The possible states for a check suite or run status. +""" +enum CheckStatusState { + """ + The check suite or run has been completed. + """ + COMPLETED + + """ + The check suite or run is in progress. + """ + IN_PROGRESS + + """ + The check suite or run is in pending state. + """ + PENDING + + """ + The check suite or run has been queued. + """ + QUEUED + + """ + The check suite or run has been requested. + """ + REQUESTED + + """ + The check suite or run is in waiting state. + """ + WAITING +} + +""" +A single check step. +""" +type CheckStep { + """ + Identifies the date and time when the check step was completed. + """ + completedAt: DateTime + + """ + The conclusion of the check step. + """ + conclusion: CheckConclusionState + + """ + A reference for the check step on the integrator's system. + """ + externalId: String + + """ + The step's name. + """ + name: String! + + """ + The index of the step in the list of steps of the parent check run. + """ + number: Int! + + """ + Number of seconds to completion. + """ + secondsToCompletion: Int + + """ + Identifies the date and time when the check step was started. + """ + startedAt: DateTime + + """ + The current status of the check step. + """ + status: CheckStatusState! +} + +""" +The connection type for CheckStep. +""" +type CheckStepConnection { + """ + A list of edges. + """ + edges: [CheckStepEdge] + + """ + A list of nodes. + """ + nodes: [CheckStep] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CheckStepEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CheckStep +} + +""" +A check suite. +""" +type CheckSuite implements Node { + """ + The GitHub App which created this check suite. + """ + app: App + + """ + The name of the branch for this check suite. + """ + branch: Ref + + """ + The check runs associated with a check suite. + """ + checkRuns( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filters the check runs by this type. + """ + filterBy: CheckRunFilter + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CheckRunConnection + + """ + The commit for this check suite + """ + commit: Commit! + + """ + The conclusion of this check suite. + """ + conclusion: CheckConclusionState + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who triggered the check suite. + """ + creator: User + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + A list of open pull requests matching the check suite. + """ + matchingPullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection + + """ + The push that triggered this check suite. + """ + push: Push + + """ + The repository associated with this check suite. + """ + repository: Repository! + + """ + The HTTP path for this check suite + """ + resourcePath: URI! + + """ + The status of this check suite. + """ + status: CheckStatusState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this check suite + """ + url: URI! + + """ + The workflow run associated with this check suite. + """ + workflowRun: WorkflowRun +} + +""" +The auto-trigger preferences that are available for check suites. +""" +input CheckSuiteAutoTriggerPreference { + """ + The node ID of the application that owns the check suite. + """ + appId: ID! + + """ + Set to `true` to enable automatic creation of CheckSuite events upon pushes to the repository. + """ + setting: Boolean! +} + +""" +The connection type for CheckSuite. +""" +type CheckSuiteConnection { + """ + A list of edges. + """ + edges: [CheckSuiteEdge] + + """ + A list of nodes. + """ + nodes: [CheckSuite] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CheckSuiteEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CheckSuite +} + +""" +The filters that are available when fetching check suites. +""" +input CheckSuiteFilter { + """ + Filters the check suites created by this application ID. + """ + appId: Int + + """ + Filters the check suites by this name. + """ + checkName: String +} + +""" +An object which can have its data claimed or claim data from another. +""" +union Claimable = Mannequin | User + +""" +Autogenerated input type of ClearLabelsFromLabelable +""" +input ClearLabelsFromLabelableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the labelable object to clear the labels from. + """ + labelableId: ID! @possibleTypes(concreteTypes: ["Discussion", "Issue", "PullRequest"], abstractType: "Labelable") +} + +""" +Autogenerated return type of ClearLabelsFromLabelable +""" +type ClearLabelsFromLabelablePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was unlabeled. + """ + labelable: Labelable +} + +""" +Autogenerated input type of ClearProjectV2ItemFieldValue +""" +input ClearProjectV2ItemFieldValueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the field to be cleared. + """ + fieldId: ID! + @possibleTypes( + concreteTypes: ["ProjectV2Field", "ProjectV2IterationField", "ProjectV2SingleSelectField"] + abstractType: "ProjectV2FieldConfiguration" + ) + + """ + The ID of the item to be cleared. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the Project. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of ClearProjectV2ItemFieldValue +""" +type ClearProjectV2ItemFieldValuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated item. + """ + projectV2Item: ProjectV2Item +} + +""" +Autogenerated input type of CloneProject +""" +input CloneProjectInput { + """ + The description of the project. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether or not to clone the source project's workflows. + """ + includeWorkflows: Boolean! + + """ + The name of the project. + """ + name: String! + + """ + The visibility of the project, defaults to false (private). + """ + public: Boolean + + """ + The source project to clone. + """ + sourceId: ID! @possibleTypes(concreteTypes: ["Project"]) + + """ + The owner ID to create the project under. + """ + targetOwnerId: ID! @possibleTypes(concreteTypes: ["Organization", "Repository", "User"], abstractType: "ProjectOwner") +} + +""" +Autogenerated return type of CloneProject +""" +type CloneProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the JobStatus for populating cloned fields. + """ + jobStatusId: String + + """ + The new cloned project. + """ + project: Project +} + +""" +Autogenerated input type of CloneTemplateRepository +""" +input CloneTemplateRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A short description of the new repository. + """ + description: String + + """ + Whether to copy all branches from the template to the new repository. Defaults + to copying only the default branch of the template. + """ + includeAllBranches: Boolean = false + + """ + The name of the new repository. + """ + name: String! + + """ + The ID of the owner for the new repository. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "RepositoryOwner") + + """ + The Node ID of the template repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + Indicates the repository's visibility level. + """ + visibility: RepositoryVisibility! +} + +""" +Autogenerated return type of CloneTemplateRepository +""" +type CloneTemplateRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new repository. + """ + repository: Repository +} + +""" +An object that can be closed +""" +interface Closable { + """ + Indicates if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! +} + +""" +Autogenerated input type of CloseDiscussion +""" +input CloseDiscussionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the discussion to be closed. + """ + discussionId: ID! @possibleTypes(concreteTypes: ["Discussion"]) + + """ + The reason why the discussion is being closed. + """ + reason: DiscussionCloseReason = RESOLVED +} + +""" +Autogenerated return type of CloseDiscussion +""" +type CloseDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion that was closed. + """ + discussion: Discussion +} + +""" +Autogenerated input type of CloseIssue +""" +input CloseIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the issue to be closed. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + The reason the issue is to be closed. + """ + stateReason: IssueClosedStateReason +} + +""" +Autogenerated return type of CloseIssue +""" +type CloseIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was closed. + """ + issue: Issue +} + +""" +Autogenerated input type of ClosePullRequest +""" +input ClosePullRequestInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to be closed. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of ClosePullRequest +""" +type ClosePullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that was closed. + """ + pullRequest: PullRequest +} + +""" +Represents a 'closed' event on any `Closable`. +""" +type ClosedEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Object that was closed. + """ + closable: Closable! + + """ + Object which triggered the creation of this event. + """ + closer: Closer + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + The HTTP path for this closed event. + """ + resourcePath: URI! + + """ + The reason the issue state was changed to closed. + """ + stateReason: IssueStateReason + + """ + The HTTP URL for this closed event. + """ + url: URI! +} + +""" +The object which triggered a `ClosedEvent`. +""" +union Closer = Commit | PullRequest + +""" +The Code of Conduct for a repository +""" +type CodeOfConduct implements Node { + """ + The body of the Code of Conduct + """ + body: String + id: ID! + + """ + The key for the Code of Conduct + """ + key: String! + + """ + The formal name of the Code of Conduct + """ + name: String! + + """ + The HTTP path for this Code of Conduct + """ + resourcePath: URI + + """ + The HTTP URL for this Code of Conduct + """ + url: URI +} + +""" +Collaborators affiliation level with a subject. +""" +enum CollaboratorAffiliation { + """ + All collaborators the authenticated user can see. + """ + ALL + + """ + All collaborators with permissions to an organization-owned subject, regardless of organization membership status. + """ + DIRECT + + """ + All outside collaborators of an organization-owned subject. + """ + OUTSIDE +} + +""" +Represents a comment. +""" +interface Comment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +A comment author association with repository. +""" +enum CommentAuthorAssociation { + """ + Author has been invited to collaborate on the repository. + """ + COLLABORATOR + + """ + Author has previously committed to the repository. + """ + CONTRIBUTOR + + """ + Author has not previously committed to GitHub. + """ + FIRST_TIMER + + """ + Author has not previously committed to the repository. + """ + FIRST_TIME_CONTRIBUTOR + + """ + Author is a placeholder for an unclaimed user. + """ + MANNEQUIN + + """ + Author is a member of the organization that owns the repository. + """ + MEMBER + + """ + Author has no association with the repository. + """ + NONE + + """ + Author is the owner of the repository. + """ + OWNER +} + +""" +The possible errors that will prevent a user from updating a comment. +""" +enum CommentCannotUpdateReason { + """ + Unable to create comment because repository is archived. + """ + ARCHIVED + + """ + You cannot update this comment + """ + DENIED + + """ + You must be the author or have write access to this repository to update this comment. + """ + INSUFFICIENT_ACCESS + + """ + Unable to create comment because issue is locked. + """ + LOCKED + + """ + You must be logged in to update this comment. + """ + LOGIN_REQUIRED + + """ + Repository is under maintenance. + """ + MAINTENANCE + + """ + At least one email address must be verified to update this comment. + """ + VERIFIED_EMAIL_REQUIRED +} + +""" +Represents a 'comment_deleted' event on a given issue or pull request. +""" +type CommentDeletedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The user who authored the deleted comment. + """ + deletedCommentAuthor: Actor + id: ID! +} + +""" +Represents a Git commit. +""" +type Commit implements GitObject & Node & Subscribable & UniformResourceLocatable { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + The number of additions in this commit. + """ + additions: Int! + + """ + The merged Pull Request that introduced the commit to the repository. If the + commit is not present in the default branch, additionally returns open Pull + Requests associated with the commit + """ + associatedPullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests. + """ + orderBy: PullRequestOrder = {field: CREATED_AT, direction: ASC} + ): PullRequestConnection + + """ + Authorship details of the commit. + """ + author: GitActor + + """ + Check if the committer and the author match. + """ + authoredByCommitter: Boolean! + + """ + The datetime when this commit was authored. + """ + authoredDate: DateTime! + + """ + The list of authors for this commit based on the git author and the Co-authored-by + message trailer. The git author will always be first. + """ + authors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): GitActorConnection! + + """ + Fetches `git blame` information. + """ + blame( + """ + The file whose Git blame information you want. + """ + path: String! + ): Blame! + + """ + We recommend using the `changedFilesIfAvailable` field instead of + `changedFiles`, as `changedFiles` will cause your request to return an error + if GitHub is unable to calculate the number of changed files. + """ + changedFiles: Int! + @deprecated( + reason: "`changedFiles` will be removed. Use `changedFilesIfAvailable` instead. Removal on 2023-01-01 UTC." + ) + + """ + The number of changed files in this commit. If GitHub is unable to calculate + the number of changed files (for example due to a timeout), this will return + `null`. We recommend using this field instead of `changedFiles`. + """ + changedFilesIfAvailable: Int + + """ + The check suites associated with a commit. + """ + checkSuites( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filters the check suites by this type. + """ + filterBy: CheckSuiteFilter + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CheckSuiteConnection + + """ + Comments made on the commit. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + + """ + The datetime when this commit was committed. + """ + committedDate: DateTime! + + """ + Check if committed via GitHub web UI. + """ + committedViaWeb: Boolean! + + """ + Committer details of the commit. + """ + committer: GitActor + + """ + The number of deletions in this commit. + """ + deletions: Int! + + """ + The deployments associated with a commit. + """ + deployments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Environments to list deployments for + """ + environments: [String!] + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for deployments returned from the connection. + """ + orderBy: DeploymentOrder = {field: CREATED_AT, direction: ASC} + ): DeploymentConnection + + """ + The tree entry representing the file located at the given path. + """ + file( + """ + The path for the file + """ + path: String! + ): TreeEntry + + """ + The linear commit history starting from (and including) this commit, in the same order as `git log`. + """ + history( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + If non-null, filters history to only show commits with matching authorship. + """ + author: CommitAuthor + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + If non-null, filters history to only show commits touching files under this path. + """ + path: String + + """ + Allows specifying a beginning time or date for fetching commits. + """ + since: GitTimestamp + + """ + Allows specifying an ending time or date for fetching commits. + """ + until: GitTimestamp + ): CommitHistoryConnection! + id: ID! + + """ + The Git commit message + """ + message: String! + + """ + The Git commit message body + """ + messageBody: String! + + """ + The commit message body rendered to HTML. + """ + messageBodyHTML: HTML! + + """ + The Git commit message headline + """ + messageHeadline: String! + + """ + The commit message headline rendered to HTML. + """ + messageHeadlineHTML: HTML! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The organization this commit was made on behalf of. + """ + onBehalfOf: Organization + + """ + The parents of a commit. + """ + parents( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitConnection! + + """ + The datetime when this commit was pushed. + """ + pushedDate: DateTime @deprecated(reason: "`pushedDate` is no longer supported. Removal on 2023-07-01 UTC.") + + """ + The Repository this commit belongs to + """ + repository: Repository! + + """ + The HTTP path for this commit + """ + resourcePath: URI! + + """ + Commit signing information, if present. + """ + signature: GitSignature + + """ + Status information for this commit + """ + status: Status + + """ + Check and Status rollup information for this commit. + """ + statusCheckRollup: StatusCheckRollup + + """ + Returns a list of all submodules in this repository as of this Commit parsed from the .gitmodules file. + """ + submodules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): SubmoduleConnection! + + """ + Returns a URL to download a tarball archive for a repository. + Note: For private repositories, these links are temporary and expire after five minutes. + """ + tarballUrl: URI! + + """ + Commit's root Tree + """ + tree: Tree! + + """ + The HTTP path for the tree of this commit + """ + treeResourcePath: URI! + + """ + The HTTP URL for the tree of this commit + """ + treeUrl: URI! + + """ + The HTTP URL for this commit + """ + url: URI! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState + + """ + Returns a URL to download a zipball archive for a repository. + Note: For private repositories, these links are temporary and expire after five minutes. + """ + zipballUrl: URI! +} + +""" +Specifies an author for filtering Git commits. +""" +input CommitAuthor { + """ + Email addresses to filter by. Commits authored by any of the specified email addresses will be returned. + """ + emails: [String!] + + """ + ID of a User to filter by. If non-null, only commits authored by this user + will be returned. This field takes precedence over emails. + """ + id: ID +} + +""" +Parameters to be used for the commit_author_email_pattern rule +""" +type CommitAuthorEmailPatternParameters { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean! + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Parameters to be used for the commit_author_email_pattern rule +""" +input CommitAuthorEmailPatternParametersInput { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Represents a comment on a given Commit. +""" +type CommitComment implements Comment & Deletable & Minimizable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Identifies the comment body. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the commit associated with the comment, if the commit exists. + """ + commit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + Identifies the file path associated with the comment. + """ + path: String + + """ + Identifies the line position associated with the comment. + """ + position: Int + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path permalink for this commit comment. + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL permalink for this commit comment. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for CommitComment. +""" +type CommitCommentConnection { + """ + A list of edges. + """ + edges: [CommitCommentEdge] + + """ + A list of nodes. + """ + nodes: [CommitComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CommitCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CommitComment +} + +""" +A thread of comments on a commit. +""" +type CommitCommentThread implements Node & RepositoryNode { + """ + The comments that exist in this thread. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + The commit the comments were made on. + """ + commit: Commit + id: ID! + + """ + The file the comments were made on. + """ + path: String + + """ + The position in the diff for the commit that the comment was made on. + """ + position: Int + + """ + The repository associated with this node. + """ + repository: Repository! +} + +""" +The connection type for Commit. +""" +type CommitConnection { + """ + A list of edges. + """ + edges: [CommitEdge] + + """ + A list of nodes. + """ + nodes: [Commit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Ordering options for commit contribution connections. +""" +input CommitContributionOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field by which to order commit contributions. + """ + field: CommitContributionOrderField! +} + +""" +Properties by which commit contribution connections can be ordered. +""" +enum CommitContributionOrderField { + """ + Order commit contributions by how many commits they represent. + """ + COMMIT_COUNT + + """ + Order commit contributions by when they were made. + """ + OCCURRED_AT +} + +""" +This aggregates commits made by a user within one repository. +""" +type CommitContributionsByRepository { + """ + The commit contributions, each representing a day. + """ + contributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for commit contributions returned from the connection. + """ + orderBy: CommitContributionOrder = {field: OCCURRED_AT, direction: DESC} + ): CreatedCommitContributionConnection! + + """ + The repository in which the commits were made. + """ + repository: Repository! + + """ + The HTTP path for the user's commits to the repository in this time range. + """ + resourcePath: URI! + + """ + The HTTP URL for the user's commits to the repository in this time range. + """ + url: URI! +} + +""" +An edge in a connection. +""" +type CommitEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Commit +} + +""" +The connection type for Commit. +""" +type CommitHistoryConnection { + """ + A list of edges. + """ + edges: [CommitEdge] + + """ + A list of nodes. + """ + nodes: [Commit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A message to include with a new commit +""" +input CommitMessage { + """ + The body of the message. + """ + body: String + + """ + The headline of the message. + """ + headline: String! +} + +""" +Parameters to be used for the commit_message_pattern rule +""" +type CommitMessagePatternParameters { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean! + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Parameters to be used for the commit_message_pattern rule +""" +input CommitMessagePatternParametersInput { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +A git ref for a commit to be appended to. + +The ref must be a branch, i.e. its fully qualified name must start +with `refs/heads/` (although the input is not required to be fully +qualified). + +The Ref may be specified by its global node ID or by the +`repositoryNameWithOwner` and `branchName`. + +### Examples + +Specify a branch using a global node ID: + + { "id": "MDM6UmVmMTpyZWZzL2hlYWRzL21haW4=" } + +Specify a branch using `repositoryNameWithOwner` and `branchName`: + + { + "repositoryNameWithOwner": "github/graphql-client", + "branchName": "main" + } +""" +input CommittableBranch { + """ + The unqualified name of the branch to append the commit to. + """ + branchName: String + + """ + The Node ID of the Ref to be updated. + """ + id: ID + + """ + The nameWithOwner of the repository to commit to. + """ + repositoryNameWithOwner: String +} + +""" +Parameters to be used for the committer_email_pattern rule +""" +type CommitterEmailPatternParameters { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean! + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Parameters to be used for the committer_email_pattern rule +""" +input CommitterEmailPatternParametersInput { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Represents a comparison between two commit revisions. +""" +type Comparison implements Node { + """ + The number of commits ahead of the base branch. + """ + aheadBy: Int! + + """ + The base revision of this comparison. + """ + baseTarget: GitObject! + + """ + The number of commits behind the base branch. + """ + behindBy: Int! + + """ + The commits which compose this comparison. + """ + commits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ComparisonCommitConnection! + + """ + The head revision of this comparison. + """ + headTarget: GitObject! + id: ID! + + """ + The status of this comparison. + """ + status: ComparisonStatus! +} + +""" +The connection type for Commit. +""" +type ComparisonCommitConnection { + """ + The total count of authors and co-authors across all commits. + """ + authorCount: Int! + + """ + A list of edges. + """ + edges: [CommitEdge] + + """ + A list of nodes. + """ + nodes: [Commit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The status of a git comparison between two refs. +""" +enum ComparisonStatus { + """ + The head ref is ahead of the base ref. + """ + AHEAD + + """ + The head ref is behind the base ref. + """ + BEHIND + + """ + The head ref is both ahead and behind of the base ref, indicating git history has diverged. + """ + DIVERGED + + """ + The head ref and base ref are identical. + """ + IDENTICAL +} + +""" +Represents a 'connected' event on a given issue or pull request. +""" +type ConnectedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Reference originated in a different repository. + """ + isCrossRepository: Boolean! + + """ + Issue or pull request that made the reference. + """ + source: ReferencedSubject! + + """ + Issue or pull request which was connected. + """ + subject: ReferencedSubject! +} + +""" +Represents a contribution a user made on GitHub, such as opening an issue. +""" +interface Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +A calendar of contributions made on GitHub by a user. +""" +type ContributionCalendar { + """ + A list of hex color codes used in this calendar. The darker the color, the more contributions it represents. + """ + colors: [String!]! + + """ + Determine if the color set was chosen because it's currently Halloween. + """ + isHalloween: Boolean! + + """ + A list of the months of contributions in this calendar. + """ + months: [ContributionCalendarMonth!]! + + """ + The count of total contributions in the calendar. + """ + totalContributions: Int! + + """ + A list of the weeks of contributions in this calendar. + """ + weeks: [ContributionCalendarWeek!]! +} + +""" +Represents a single day of contributions on GitHub by a user. +""" +type ContributionCalendarDay { + """ + The hex color code that represents how many contributions were made on this day compared to others in the calendar. + """ + color: String! + + """ + How many contributions were made by the user on this day. + """ + contributionCount: Int! + + """ + Indication of contributions, relative to other days. Can be used to indicate + which color to represent this day on a calendar. + """ + contributionLevel: ContributionLevel! + + """ + The day this square represents. + """ + date: Date! + + """ + A number representing which day of the week this square represents, e.g., 1 is Monday. + """ + weekday: Int! +} + +""" +A month of contributions in a user's contribution graph. +""" +type ContributionCalendarMonth { + """ + The date of the first day of this month. + """ + firstDay: Date! + + """ + The name of the month. + """ + name: String! + + """ + How many weeks started in this month. + """ + totalWeeks: Int! + + """ + The year the month occurred in. + """ + year: Int! +} + +""" +A week of contributions in a user's contribution graph. +""" +type ContributionCalendarWeek { + """ + The days of contributions in this week. + """ + contributionDays: [ContributionCalendarDay!]! + + """ + The date of the earliest square in this week. + """ + firstDay: Date! +} + +""" +Varying levels of contributions from none to many. +""" +enum ContributionLevel { + """ + Lowest 25% of days of contributions. + """ + FIRST_QUARTILE + + """ + Highest 25% of days of contributions. More contributions than the third quartile. + """ + FOURTH_QUARTILE + + """ + No contributions occurred. + """ + NONE + + """ + Second lowest 25% of days of contributions. More contributions than the first quartile. + """ + SECOND_QUARTILE + + """ + Second highest 25% of days of contributions. More contributions than second quartile, less than the fourth quartile. + """ + THIRD_QUARTILE +} + +""" +Ordering options for contribution connections. +""" +input ContributionOrder { + """ + The ordering direction. + """ + direction: OrderDirection! +} + +""" +A contributions collection aggregates contributions such as opened issues and commits created by a user. +""" +type ContributionsCollection { + """ + Commit contributions made by the user, grouped by repository. + """ + commitContributionsByRepository( + """ + How many repositories should be included. + """ + maxRepositories: Int = 25 + ): [CommitContributionsByRepository!]! + + """ + A calendar of this user's contributions on GitHub. + """ + contributionCalendar: ContributionCalendar! + + """ + The years the user has been making contributions with the most recent year first. + """ + contributionYears: [Int!]! + + """ + Determine if this collection's time span ends in the current month. + """ + doesEndInCurrentMonth: Boolean! + + """ + The date of the first restricted contribution the user made in this time + period. Can only be non-null when the user has enabled private contribution counts. + """ + earliestRestrictedContributionDate: Date + + """ + The ending date and time of this collection. + """ + endedAt: DateTime! + + """ + The first issue the user opened on GitHub. This will be null if that issue was + opened outside the collection's time range and ignoreTimeRange is false. If + the issue is not visible but the user has opted to show private contributions, + a RestrictedContribution will be returned. + """ + firstIssueContribution: CreatedIssueOrRestrictedContribution + + """ + The first pull request the user opened on GitHub. This will be null if that + pull request was opened outside the collection's time range and + ignoreTimeRange is not true. If the pull request is not visible but the user + has opted to show private contributions, a RestrictedContribution will be returned. + """ + firstPullRequestContribution: CreatedPullRequestOrRestrictedContribution + + """ + The first repository the user created on GitHub. This will be null if that + first repository was created outside the collection's time range and + ignoreTimeRange is false. If the repository is not visible, then a + RestrictedContribution is returned. + """ + firstRepositoryContribution: CreatedRepositoryOrRestrictedContribution + + """ + Does the user have any more activity in the timeline that occurred prior to the collection's time range? + """ + hasActivityInThePast: Boolean! + + """ + Determine if there are any contributions in this collection. + """ + hasAnyContributions: Boolean! + + """ + Determine if the user made any contributions in this time frame whose details + are not visible because they were made in a private repository. Can only be + true if the user enabled private contribution counts. + """ + hasAnyRestrictedContributions: Boolean! + + """ + Whether or not the collector's time span is all within the same day. + """ + isSingleDay: Boolean! + + """ + A list of issues the user opened. + """ + issueContributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Should the user's first issue ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented issue be excluded from the result. + """ + excludePopular: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedIssueContributionConnection! + + """ + Issue contributions made by the user, grouped by repository. + """ + issueContributionsByRepository( + """ + Should the user's first issue ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented issue be excluded from the result. + """ + excludePopular: Boolean = false + + """ + How many repositories should be included. + """ + maxRepositories: Int = 25 + ): [IssueContributionsByRepository!]! + + """ + When the user signed up for GitHub. This will be null if that sign up date + falls outside the collection's time range and ignoreTimeRange is false. + """ + joinedGitHubContribution: JoinedGitHubContribution + + """ + The date of the most recent restricted contribution the user made in this time + period. Can only be non-null when the user has enabled private contribution counts. + """ + latestRestrictedContributionDate: Date + + """ + When this collection's time range does not include any activity from the user, use this + to get a different collection from an earlier time range that does have activity. + """ + mostRecentCollectionWithActivity: ContributionsCollection + + """ + Returns a different contributions collection from an earlier time range than this one + that does not have any contributions. + """ + mostRecentCollectionWithoutActivity: ContributionsCollection + + """ + The issue the user opened on GitHub that received the most comments in the specified + time frame. + """ + popularIssueContribution: CreatedIssueContribution + + """ + The pull request the user opened on GitHub that received the most comments in the + specified time frame. + """ + popularPullRequestContribution: CreatedPullRequestContribution + + """ + Pull request contributions made by the user. + """ + pullRequestContributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Should the user's first pull request ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented pull request be excluded from the result. + """ + excludePopular: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedPullRequestContributionConnection! + + """ + Pull request contributions made by the user, grouped by repository. + """ + pullRequestContributionsByRepository( + """ + Should the user's first pull request ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented pull request be excluded from the result. + """ + excludePopular: Boolean = false + + """ + How many repositories should be included. + """ + maxRepositories: Int = 25 + ): [PullRequestContributionsByRepository!]! + + """ + Pull request review contributions made by the user. Returns the most recently + submitted review for each PR reviewed by the user. + """ + pullRequestReviewContributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedPullRequestReviewContributionConnection! + + """ + Pull request review contributions made by the user, grouped by repository. + """ + pullRequestReviewContributionsByRepository( + """ + How many repositories should be included. + """ + maxRepositories: Int = 25 + ): [PullRequestReviewContributionsByRepository!]! + + """ + A list of repositories owned by the user that the user created in this time range. + """ + repositoryContributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Should the user's first repository ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedRepositoryContributionConnection! + + """ + A count of contributions made by the user that the viewer cannot access. Only + non-zero when the user has chosen to share their private contribution counts. + """ + restrictedContributionsCount: Int! + + """ + The beginning date and time of this collection. + """ + startedAt: DateTime! + + """ + How many commits were made by the user in this time span. + """ + totalCommitContributions: Int! + + """ + How many issues the user opened. + """ + totalIssueContributions( + """ + Should the user's first issue ever be excluded from this count. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented issue be excluded from this count. + """ + excludePopular: Boolean = false + ): Int! + + """ + How many pull requests the user opened. + """ + totalPullRequestContributions( + """ + Should the user's first pull request ever be excluded from this count. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented pull request be excluded from this count. + """ + excludePopular: Boolean = false + ): Int! + + """ + How many pull request reviews the user left. + """ + totalPullRequestReviewContributions: Int! + + """ + How many different repositories the user committed to. + """ + totalRepositoriesWithContributedCommits: Int! + + """ + How many different repositories the user opened issues in. + """ + totalRepositoriesWithContributedIssues( + """ + Should the user's first issue ever be excluded from this count. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented issue be excluded from this count. + """ + excludePopular: Boolean = false + ): Int! + + """ + How many different repositories the user left pull request reviews in. + """ + totalRepositoriesWithContributedPullRequestReviews: Int! + + """ + How many different repositories the user opened pull requests in. + """ + totalRepositoriesWithContributedPullRequests( + """ + Should the user's first pull request ever be excluded from this count. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented pull request be excluded from this count. + """ + excludePopular: Boolean = false + ): Int! + + """ + How many repositories the user created. + """ + totalRepositoryContributions( + """ + Should the user's first repository ever be excluded from this count. + """ + excludeFirst: Boolean = false + ): Int! + + """ + The user who made the contributions in this collection. + """ + user: User! +} + +""" +Autogenerated input type of ConvertProjectCardNoteToIssue +""" +input ConvertProjectCardNoteToIssueInput { + """ + The body of the newly created issue. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ProjectCard ID to convert. + """ + projectCardId: ID! @possibleTypes(concreteTypes: ["ProjectCard"]) + + """ + The ID of the repository to create the issue in. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The title of the newly created issue. Defaults to the card's note text. + """ + title: String +} + +""" +Autogenerated return type of ConvertProjectCardNoteToIssue +""" +type ConvertProjectCardNoteToIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated ProjectCard. + """ + projectCard: ProjectCard +} + +""" +Autogenerated input type of ConvertPullRequestToDraft +""" +input ConvertPullRequestToDraftInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to convert to draft + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of ConvertPullRequestToDraft +""" +type ConvertPullRequestToDraftPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that is now a draft. + """ + pullRequest: PullRequest +} + +""" +Represents a 'convert_to_draft' event on a given pull request. +""" +type ConvertToDraftEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + The HTTP path for this convert to draft event. + """ + resourcePath: URI! + + """ + The HTTP URL for this convert to draft event. + """ + url: URI! +} + +""" +Represents a 'converted_note_to_issue' event on a given issue or pull request. +""" +type ConvertedNoteToIssueEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Project referenced by event. + """ + project: Project @preview(toggledBy: "starfox-preview") + + """ + Project card referenced by this project event. + """ + projectCard: ProjectCard @preview(toggledBy: "starfox-preview") + + """ + Column name referenced by this project event. + """ + projectColumnName: String! @preview(toggledBy: "starfox-preview") +} + +""" +Represents a 'converted_to_discussion' event on a given issue. +""" +type ConvertedToDiscussionEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The discussion that the issue was converted into. + """ + discussion: Discussion + id: ID! +} + +""" +Autogenerated input type of CopyProjectV2 +""" +input CopyProjectV2Input { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Include draft issues in the new project + """ + includeDraftIssues: Boolean = false + + """ + The owner ID of the new project. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "OrganizationOrUser") + + """ + The ID of the source Project to copy. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The title of the project. + """ + title: String! +} + +""" +Autogenerated return type of CopyProjectV2 +""" +type CopyProjectV2Payload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The copied project. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of CreateAttributionInvitation +""" +input CreateAttributionInvitationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the owner scoping the reattributable data. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Bot", "Enterprise", "Mannequin", "Organization", "User"]) + + """ + The Node ID of the account owning the data to reattribute. + """ + sourceId: ID! @possibleTypes(concreteTypes: ["Bot", "Enterprise", "Mannequin", "Organization", "User"]) + + """ + The Node ID of the account which may claim the data. + """ + targetId: ID! @possibleTypes(concreteTypes: ["Bot", "Enterprise", "Mannequin", "Organization", "User"]) +} + +""" +Autogenerated return type of CreateAttributionInvitation +""" +type CreateAttributionInvitationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The owner scoping the reattributable data. + """ + owner: Organization + + """ + The account owning the data to reattribute. + """ + source: Claimable + + """ + The account which may claim the data. + """ + target: Claimable +} + +""" +Autogenerated input type of CreateBranchProtectionRule +""" +input CreateBranchProtectionRuleInput { + """ + Can this branch be deleted. + """ + allowsDeletions: Boolean + + """ + Are force pushes allowed on this branch. + """ + allowsForcePushes: Boolean + + """ + Is branch creation a protected operation. + """ + blocksCreations: Boolean + + """ + A list of User, Team, or App IDs allowed to bypass force push targeting matching branches. + """ + bypassForcePushActorIds: [ID!] + + """ + A list of User, Team, or App IDs allowed to bypass pull requests targeting matching branches. + """ + bypassPullRequestActorIds: [ID!] + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Will new commits pushed to matching branches dismiss pull request review approvals. + """ + dismissesStaleReviews: Boolean + + """ + Can admins overwrite branch protection. + """ + isAdminEnforced: Boolean + + """ + Whether users can pull changes from upstream when the branch is locked. Set to + `true` to allow fork syncing. Set to `false` to prevent fork syncing. + """ + lockAllowsFetchAndMerge: Boolean + + """ + Whether to set the branch as read-only. If this is true, users will not be able to push to the branch. + """ + lockBranch: Boolean + + """ + The glob-like pattern used to determine matching branches. + """ + pattern: String! + + """ + A list of User, Team, or App IDs allowed to push to matching branches. + """ + pushActorIds: [ID!] + + """ + The global relay id of the repository in which a new branch protection rule should be created in. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + Whether the most recent push must be approved by someone other than the person who pushed it + """ + requireLastPushApproval: Boolean + + """ + Number of approving reviews required to update matching branches. + """ + requiredApprovingReviewCount: Int + + """ + The list of required deployment environments + """ + requiredDeploymentEnvironments: [String!] + + """ + List of required status check contexts that must pass for commits to be accepted to matching branches. + """ + requiredStatusCheckContexts: [String!] + + """ + The list of required status checks + """ + requiredStatusChecks: [RequiredStatusCheckInput!] + + """ + Are approving reviews required to update matching branches. + """ + requiresApprovingReviews: Boolean + + """ + Are reviews from code owners required to update matching branches. + """ + requiresCodeOwnerReviews: Boolean + + """ + Are commits required to be signed. + """ + requiresCommitSignatures: Boolean + + """ + Are conversations required to be resolved before merging. + """ + requiresConversationResolution: Boolean + + """ + Are successful deployments required before merging. + """ + requiresDeployments: Boolean + + """ + Are merge commits prohibited from being pushed to this branch. + """ + requiresLinearHistory: Boolean + + """ + Are status checks required to update matching branches. + """ + requiresStatusChecks: Boolean + + """ + Are branches required to be up to date before merging. + """ + requiresStrictStatusChecks: Boolean + + """ + Is pushing to matching branches restricted. + """ + restrictsPushes: Boolean + + """ + Is dismissal of pull request reviews restricted. + """ + restrictsReviewDismissals: Boolean + + """ + A list of User, Team, or App IDs allowed to dismiss reviews on pull requests targeting matching branches. + """ + reviewDismissalActorIds: [ID!] +} + +""" +Autogenerated return type of CreateBranchProtectionRule +""" +type CreateBranchProtectionRulePayload { + """ + The newly created BranchProtectionRule. + """ + branchProtectionRule: BranchProtectionRule + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of CreateCheckRun +""" +input CreateCheckRunInput { + """ + Possible further actions the integrator can perform, which a user may trigger. + """ + actions: [CheckRunAction!] + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The time that the check run finished. + """ + completedAt: DateTime + + """ + The final conclusion of the check. + """ + conclusion: CheckConclusionState + + """ + The URL of the integrator's site that has the full details of the check. + """ + detailsUrl: URI + + """ + A reference for the run on the integrator's system. + """ + externalId: String + + """ + The SHA of the head commit. + """ + headSha: GitObjectID! + + """ + The name of the check. + """ + name: String! + + """ + Descriptive details about the run. + """ + output: CheckRunOutput + + """ + The node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The time that the check run began. + """ + startedAt: DateTime + + """ + The current status. + """ + status: RequestableCheckStatusState +} + +""" +Autogenerated return type of CreateCheckRun +""" +type CreateCheckRunPayload { + """ + The newly created check run. + """ + checkRun: CheckRun + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of CreateCheckSuite +""" +input CreateCheckSuiteInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The SHA of the head commit. + """ + headSha: GitObjectID! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateCheckSuite +""" +type CreateCheckSuitePayload { + """ + The newly created check suite. + """ + checkSuite: CheckSuite + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of CreateCommitOnBranch +""" +input CreateCommitOnBranchInput { + """ + The Ref to be updated. Must be a branch. + """ + branch: CommittableBranch! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The git commit oid expected at the head of the branch prior to the commit + """ + expectedHeadOid: GitObjectID! + + """ + A description of changes to files in this commit. + """ + fileChanges: FileChanges + + """ + The commit message the be included with the commit. + """ + message: CommitMessage! +} + +""" +Autogenerated return type of CreateCommitOnBranch +""" +type CreateCommitOnBranchPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new commit. + """ + commit: Commit + + """ + The ref which has been updated to point to the new commit. + """ + ref: Ref +} + +""" +Autogenerated input type of CreateDeployment +""" +input CreateDeploymentInput @preview(toggledBy: "flash-preview") { + """ + Attempt to automatically merge the default branch into the requested ref, defaults to true. + """ + autoMerge: Boolean = true + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Short description of the deployment. + """ + description: String = "" + + """ + Name for the target deployment environment. + """ + environment: String = "production" + + """ + JSON payload with extra information about the deployment. + """ + payload: String = "{}" + + """ + The node ID of the ref to be deployed. + """ + refId: ID! @possibleTypes(concreteTypes: ["Ref"]) + + """ + The node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The status contexts to verify against commit status checks. To bypass required + contexts, pass an empty array. Defaults to all unique contexts. + """ + requiredContexts: [String!] + + """ + Specifies a task to execute. + """ + task: String = "deploy" +} + +""" +Autogenerated return type of CreateDeployment +""" +type CreateDeploymentPayload @preview(toggledBy: "flash-preview") { + """ + True if the default branch has been auto-merged into the deployment ref. + """ + autoMerged: Boolean + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new deployment. + """ + deployment: Deployment +} + +""" +Autogenerated input type of CreateDeploymentStatus +""" +input CreateDeploymentStatusInput @preview(toggledBy: "flash-preview") { + """ + Adds a new inactive status to all non-transient, non-production environment + deployments with the same repository and environment name as the created + status's deployment. + """ + autoInactive: Boolean = true + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The node ID of the deployment. + """ + deploymentId: ID! @possibleTypes(concreteTypes: ["Deployment"]) + + """ + A short description of the status. Maximum length of 140 characters. + """ + description: String = "" + + """ + If provided, updates the environment of the deploy. Otherwise, does not modify the environment. + """ + environment: String + + """ + Sets the URL for accessing your environment. + """ + environmentUrl: String = "" + + """ + The log URL to associate with this status. This URL should contain + output to keep the user updated while the task is running or serve as + historical information for what happened in the deployment. + """ + logUrl: String = "" + + """ + The state of the deployment. + """ + state: DeploymentStatusState! +} + +""" +Autogenerated return type of CreateDeploymentStatus +""" +type CreateDeploymentStatusPayload @preview(toggledBy: "flash-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new deployment status. + """ + deploymentStatus: DeploymentStatus +} + +""" +Autogenerated input type of CreateDiscussion +""" +input CreateDiscussionInput { + """ + The body of the discussion. + """ + body: String! + + """ + The id of the discussion category to associate with this discussion. + """ + categoryId: ID! @possibleTypes(concreteTypes: ["DiscussionCategory"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the repository on which to create the discussion. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The title of the discussion. + """ + title: String! +} + +""" +Autogenerated return type of CreateDiscussion +""" +type CreateDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion that was just created. + """ + discussion: Discussion +} + +""" +Autogenerated input type of CreateEnterpriseOrganization +""" +input CreateEnterpriseOrganizationInput { + """ + The logins for the administrators of the new organization. + """ + adminLogins: [String!]! + + """ + The email used for sending billing receipts. + """ + billingEmail: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise owning the new organization. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of the new organization. + """ + login: String! + + """ + The profile name of the new organization. + """ + profileName: String! +} + +""" +Autogenerated return type of CreateEnterpriseOrganization +""" +type CreateEnterpriseOrganizationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise that owns the created organization. + """ + enterprise: Enterprise + + """ + The organization that was created. + """ + organization: Organization +} + +""" +Autogenerated input type of CreateEnvironment +""" +input CreateEnvironmentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the environment. + """ + name: String! + + """ + The node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateEnvironment +""" +type CreateEnvironmentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new or existing environment. + """ + environment: Environment +} + +""" +Autogenerated input type of CreateIpAllowListEntry +""" +input CreateIpAllowListEntryInput { + """ + An IP address or range of addresses in CIDR notation. + """ + allowListValue: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether the IP allow list entry is active when an IP allow list is enabled. + """ + isActive: Boolean! + + """ + An optional name for the IP allow list entry. + """ + name: String + + """ + The ID of the owner for which to create the new IP allow list entry. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["App", "Enterprise", "Organization"], abstractType: "IpAllowListOwner") +} + +""" +Autogenerated return type of CreateIpAllowListEntry +""" +type CreateIpAllowListEntryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list entry that was created. + """ + ipAllowListEntry: IpAllowListEntry +} + +""" +Autogenerated input type of CreateIssue +""" +input CreateIssueInput { + """ + The Node ID for the user assignee for this issue. + """ + assigneeIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The body for the issue description. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of an issue template in the repository, assigns labels and assignees from the template to the issue + """ + issueTemplate: String + + """ + An array of Node IDs of labels for this issue. + """ + labelIds: [ID!] @possibleTypes(concreteTypes: ["Label"]) + + """ + The Node ID of the milestone for this issue. + """ + milestoneId: ID @possibleTypes(concreteTypes: ["Milestone"]) + + """ + An array of Node IDs for projects associated with this issue. + """ + projectIds: [ID!] @possibleTypes(concreteTypes: ["Project"]) + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The title for the issue. + """ + title: String! +} + +""" +Autogenerated return type of CreateIssue +""" +type CreateIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new issue. + """ + issue: Issue +} + +""" +Autogenerated input type of CreateLabel +""" +input CreateLabelInput @preview(toggledBy: "bane-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A 6 character hex code, without the leading #, identifying the color of the label. + """ + color: String! + + """ + A brief description of the label, such as its purpose. + """ + description: String + + """ + The name of the label. + """ + name: String! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateLabel +""" +type CreateLabelPayload @preview(toggledBy: "bane-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new label. + """ + label: Label +} + +""" +Autogenerated input type of CreateLinkedBranch +""" +input CreateLinkedBranchInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the issue to link to. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + The name of the new branch. Defaults to issue number and title. + """ + name: String + + """ + The commit SHA to base the new branch on. + """ + oid: GitObjectID! + + """ + ID of the repository to create the branch in. Defaults to the issue repository. + """ + repositoryId: ID @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateLinkedBranch +""" +type CreateLinkedBranchPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new branch issue reference. + """ + linkedBranch: LinkedBranch +} + +""" +Autogenerated input type of CreateMigrationSource +""" +input CreateMigrationSourceInput { + """ + The migration source access token. + """ + accessToken: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The GitHub personal access token of the user importing to the target repository. + """ + githubPat: String + + """ + The migration source name. + """ + name: String! + + """ + The ID of the organization that will own the migration source. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + The migration source type. + """ + type: MigrationSourceType! + + """ + The migration source URL, for example `https://github.com` or `https://monalisa.ghe.com`. + """ + url: String +} + +""" +Autogenerated return type of CreateMigrationSource +""" +type CreateMigrationSourcePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The created migration source. + """ + migrationSource: MigrationSource +} + +""" +Autogenerated input type of CreateProject +""" +input CreateProjectInput { + """ + The description of project. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of project. + """ + name: String! + + """ + The owner ID to create the project under. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization", "Repository", "User"], abstractType: "ProjectOwner") + + """ + A list of repository IDs to create as linked repositories for the project + """ + repositoryIds: [ID!] @possibleTypes(concreteTypes: ["Repository"]) + + """ + The name of the GitHub-provided template. + """ + template: ProjectTemplate +} + +""" +Autogenerated return type of CreateProject +""" +type CreateProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new project. + """ + project: Project +} + +""" +Autogenerated input type of CreateProjectV2Field +""" +input CreateProjectV2FieldInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The data type of the field. + """ + dataType: ProjectV2CustomFieldType! + + """ + The name of the field. + """ + name: String! + + """ + The ID of the Project to create the field in. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + Options for a single select field. At least one value is required if data_type is SINGLE_SELECT + """ + singleSelectOptions: [ProjectV2SingleSelectFieldOptionInput!] +} + +""" +Autogenerated return type of CreateProjectV2Field +""" +type CreateProjectV2FieldPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new field. + """ + projectV2Field: ProjectV2FieldConfiguration +} + +""" +Autogenerated input type of CreateProjectV2 +""" +input CreateProjectV2Input { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The owner ID to create the project under. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "OrganizationOrUser") + + """ + The repository to link the project to. + """ + repositoryId: ID @possibleTypes(concreteTypes: ["Repository"]) + + """ + The team to link the project to. The team will be granted read permissions. + """ + teamId: ID @possibleTypes(concreteTypes: ["Team"]) + + """ + The title of the project. + """ + title: String! +} + +""" +Autogenerated return type of CreateProjectV2 +""" +type CreateProjectV2Payload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new project. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of CreatePullRequest +""" +input CreatePullRequestInput { + """ + The name of the branch you want your changes pulled into. This should be an existing branch + on the current repository. You cannot update the base branch on a pull request to point + to another repository. + """ + baseRefName: String! + + """ + The contents of the pull request. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Indicates whether this pull request should be a draft. + """ + draft: Boolean = false + + """ + The name of the branch where your changes are implemented. For cross-repository pull requests + in the same network, namespace `head_ref_name` with a user like this: `username:branch`. + """ + headRefName: String! + + """ + The Node ID of the head repository. + """ + headRepositoryId: ID @possibleTypes(concreteTypes: ["Repository"]) + + """ + Indicates whether maintainers can modify the pull request. + """ + maintainerCanModify: Boolean = true + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The title of the pull request. + """ + title: String! +} + +""" +Autogenerated return type of CreatePullRequest +""" +type CreatePullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of CreateRef +""" +input CreateRefInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The fully qualified name of the new Ref (ie: `refs/heads/my_new_branch`). + """ + name: String! + + """ + The GitObjectID that the new Ref shall target. Must point to a commit. + """ + oid: GitObjectID! + + """ + The Node ID of the Repository to create the Ref in. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateRef +""" +type CreateRefPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created ref. + """ + ref: Ref +} + +""" +Autogenerated input type of CreateRepository +""" +input CreateRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A short description of the new repository. + """ + description: String + + """ + Indicates if the repository should have the issues feature enabled. + """ + hasIssuesEnabled: Boolean = true + + """ + Indicates if the repository should have the wiki feature enabled. + """ + hasWikiEnabled: Boolean = false + + """ + The URL for a web page about this repository. + """ + homepageUrl: URI + + """ + The name of the new repository. + """ + name: String! + + """ + The ID of the owner for the new repository. + """ + ownerId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "RepositoryOwner") + + """ + When an organization is specified as the owner, this ID identifies the team + that should be granted access to the new repository. + """ + teamId: ID @possibleTypes(concreteTypes: ["Team"]) + + """ + Whether this repository should be marked as a template such that anyone who + can access it can create new repositories with the same files and directory structure. + """ + template: Boolean = false + + """ + Indicates the repository's visibility level. + """ + visibility: RepositoryVisibility! +} + +""" +Autogenerated return type of CreateRepository +""" +type CreateRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new repository. + """ + repository: Repository +} + +""" +Autogenerated input type of CreateRepositoryRuleset +""" +input CreateRepositoryRulesetInput { + """ + A list of actors that are allowed to bypass rules in this ruleset. + """ + bypassActors: [RepositoryRulesetBypassActorInput!] + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The set of conditions for this ruleset + """ + conditions: RepositoryRuleConditionsInput! + + """ + The enforcement level for this ruleset + """ + enforcement: RuleEnforcement! + + """ + The name of the ruleset. + """ + name: String! + + """ + The list of rules for this ruleset + """ + rules: [RepositoryRuleInput!] + + """ + The global relay id of the source in which a new ruleset should be created in. + """ + sourceId: ID! @possibleTypes(concreteTypes: ["Organization", "Repository"], abstractType: "RuleSource") + + """ + The target of the ruleset. + """ + target: RepositoryRulesetTarget +} + +""" +Autogenerated return type of CreateRepositoryRuleset +""" +type CreateRepositoryRulesetPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created Ruleset. + """ + ruleset: RepositoryRuleset +} + +""" +Autogenerated input type of CreateSponsorsListing +""" +input CreateSponsorsListingInput { + """ + The country or region where the sponsorable's bank account is located. + Required if fiscalHostLogin is not specified, ignored when fiscalHostLogin is specified. + """ + billingCountryOrRegionCode: SponsorsCountryOrRegionCode + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The email address we should use to contact you about the GitHub Sponsors + profile being created. This will not be shared publicly. Must be a verified + email address already on your GitHub account. Only relevant when the + sponsorable is yourself. Defaults to your primary email address on file if omitted. + """ + contactEmail: String + + """ + The username of the supported fiscal host's GitHub organization, if you want + to receive sponsorship payouts through a fiscal host rather than directly to a + bank account. For example, 'Open-Source-Collective' for Open Source Collective + or 'numfocus' for numFOCUS. Case insensitive. See https://docs.github.com/sponsors/receiving-sponsorships-through-github-sponsors/using-a-fiscal-host-to-receive-github-sponsors-payouts + for more information. + """ + fiscalHostLogin: String + + """ + The URL for your profile page on the fiscal host's website, e.g., + https://opencollective.com/babel or https://numfocus.org/project/bokeh. + Required if fiscalHostLogin is specified. + """ + fiscallyHostedProjectProfileUrl: String + + """ + Provide an introduction to serve as the main focus that appears on your GitHub + Sponsors profile. It's a great opportunity to help potential sponsors learn + more about you, your work, and why their sponsorship is important to you. + GitHub-flavored Markdown is supported. + """ + fullDescription: String + + """ + The country or region where the sponsorable resides. This is for tax purposes. + Required if the sponsorable is yourself, ignored when sponsorableLogin + specifies an organization. + """ + residenceCountryOrRegionCode: SponsorsCountryOrRegionCode + + """ + The username of the organization to create a GitHub Sponsors profile for, if + desired. Defaults to creating a GitHub Sponsors profile for the authenticated + user if omitted. + """ + sponsorableLogin: String +} + +""" +Autogenerated return type of CreateSponsorsListing +""" +type CreateSponsorsListingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new GitHub Sponsors profile. + """ + sponsorsListing: SponsorsListing +} + +""" +Autogenerated input type of CreateSponsorsTier +""" +input CreateSponsorsTierInput { + """ + The value of the new tier in US dollars. Valid values: 1-12000. + """ + amount: Int! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A description of what this tier is, what perks sponsors might receive, what a sponsorship at this tier means for you, etc. + """ + description: String! + + """ + Whether sponsorships using this tier should happen monthly/yearly or just once. + """ + isRecurring: Boolean = true + + """ + Whether to make the tier available immediately for sponsors to choose. + Defaults to creating a draft tier that will not be publicly visible. + """ + publish: Boolean = false + + """ + Optional ID of the private repository that sponsors at this tier should gain + read-only access to. Must be owned by an organization. + """ + repositoryId: ID @possibleTypes(concreteTypes: ["Repository"]) + + """ + Optional name of the private repository that sponsors at this tier should gain + read-only access to. Must be owned by an organization. Necessary if + repositoryOwnerLogin is given. Will be ignored if repositoryId is given. + """ + repositoryName: String + + """ + Optional login of the organization owner of the private repository that + sponsors at this tier should gain read-only access to. Necessary if + repositoryName is given. Will be ignored if repositoryId is given. + """ + repositoryOwnerLogin: String + + """ + The ID of the user or organization who owns the GitHub Sponsors profile. + Defaults to the current user if omitted and sponsorableLogin is not given. + """ + sponsorableId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsorable") + + """ + The username of the user or organization who owns the GitHub Sponsors profile. + Defaults to the current user if omitted and sponsorableId is not given. + """ + sponsorableLogin: String + + """ + Optional message new sponsors at this tier will receive. + """ + welcomeMessage: String +} + +""" +Autogenerated return type of CreateSponsorsTier +""" +type CreateSponsorsTierPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new tier. + """ + sponsorsTier: SponsorsTier +} + +""" +Autogenerated input type of CreateSponsorship +""" +input CreateSponsorshipInput { + """ + The amount to pay to the sponsorable in US dollars. Required if a tierId is not specified. Valid values: 1-12000. + """ + amount: Int + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether the sponsorship should happen monthly/yearly or just this one time. Required if a tierId is not specified. + """ + isRecurring: Boolean + + """ + Specify whether others should be able to see that the sponsor is sponsoring + the sponsorable. Public visibility still does not reveal which tier is used. + """ + privacyLevel: SponsorshipPrivacy = PUBLIC + + """ + Whether the sponsor should receive email updates from the sponsorable. + """ + receiveEmails: Boolean = true + + """ + The ID of the user or organization who is acting as the sponsor, paying for + the sponsorship. Required if sponsorLogin is not given. + """ + sponsorId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsor") + + """ + The username of the user or organization who is acting as the sponsor, paying + for the sponsorship. Required if sponsorId is not given. + """ + sponsorLogin: String + + """ + The ID of the user or organization who is receiving the sponsorship. Required if sponsorableLogin is not given. + """ + sponsorableId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsorable") + + """ + The username of the user or organization who is receiving the sponsorship. Required if sponsorableId is not given. + """ + sponsorableLogin: String + + """ + The ID of one of sponsorable's existing tiers to sponsor at. Required if amount is not specified. + """ + tierId: ID @possibleTypes(concreteTypes: ["SponsorsTier"]) +} + +""" +Autogenerated return type of CreateSponsorship +""" +type CreateSponsorshipPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The sponsorship that was started. + """ + sponsorship: Sponsorship +} + +""" +Autogenerated input type of CreateSponsorships +""" +input CreateSponsorshipsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Specify whether others should be able to see that the sponsor is sponsoring + the sponsorables. Public visibility still does not reveal the dollar value of + the sponsorship. + """ + privacyLevel: SponsorshipPrivacy = PUBLIC + + """ + Whether the sponsor should receive email updates from the sponsorables. + """ + receiveEmails: Boolean = false + + """ + The username of the user or organization who is acting as the sponsor, paying for the sponsorships. + """ + sponsorLogin: String! + + """ + The list of maintainers to sponsor and for how much apiece. + """ + sponsorships: [BulkSponsorship!]! +} + +""" +Autogenerated return type of CreateSponsorships +""" +type CreateSponsorshipsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The users and organizations who received a sponsorship. + """ + sponsorables: [Sponsorable!] +} + +""" +Autogenerated input type of CreateTeamDiscussionComment +""" +input CreateTeamDiscussionCommentInput { + """ + The content of the comment. This field is required. + + **Upcoming Change on 2024-07-01 UTC** + **Description:** `body` will be removed. Follow the guide at + https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to + find a suitable replacement. + **Reason:** The Team Discussions feature is deprecated in favor of Organization Discussions. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the discussion to which the comment belongs. This field is required. + + **Upcoming Change on 2024-07-01 UTC** + **Description:** `discussionId` will be removed. Follow the guide at + https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to + find a suitable replacement. + **Reason:** The Team Discussions feature is deprecated in favor of Organization Discussions. + """ + discussionId: ID @possibleTypes(concreteTypes: ["TeamDiscussion"]) +} + +""" +Autogenerated return type of CreateTeamDiscussionComment +""" +type CreateTeamDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new comment. + """ + teamDiscussionComment: TeamDiscussionComment + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) +} + +""" +Autogenerated input type of CreateTeamDiscussion +""" +input CreateTeamDiscussionInput { + """ + The content of the discussion. This field is required. + + **Upcoming Change on 2024-07-01 UTC** + **Description:** `body` will be removed. Follow the guide at + https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to + find a suitable replacement. + **Reason:** The Team Discussions feature is deprecated in favor of Organization Discussions. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + If true, restricts the visibility of this discussion to team members and + organization admins. If false or not specified, allows any organization member + to view this discussion. + + **Upcoming Change on 2024-07-01 UTC** + **Description:** `private` will be removed. Follow the guide at + https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to + find a suitable replacement. + **Reason:** The Team Discussions feature is deprecated in favor of Organization Discussions. + """ + private: Boolean + + """ + The ID of the team to which the discussion belongs. This field is required. + + **Upcoming Change on 2024-07-01 UTC** + **Description:** `teamId` will be removed. Follow the guide at + https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to + find a suitable replacement. + **Reason:** The Team Discussions feature is deprecated in favor of Organization Discussions. + """ + teamId: ID @possibleTypes(concreteTypes: ["Team"]) + + """ + The title of the discussion. This field is required. + + **Upcoming Change on 2024-07-01 UTC** + **Description:** `title` will be removed. Follow the guide at + https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to + find a suitable replacement. + **Reason:** The Team Discussions feature is deprecated in favor of Organization Discussions. + """ + title: String +} + +""" +Autogenerated return type of CreateTeamDiscussion +""" +type CreateTeamDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new discussion. + """ + teamDiscussion: TeamDiscussion + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) +} + +""" +Represents the contribution a user made by committing to a repository. +""" +type CreatedCommitContribution implements Contribution { + """ + How many commits were made on this day to this repository by the user. + """ + commitCount: Int! + + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The repository the user made a commit in. + """ + repository: Repository! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedCommitContribution. +""" +type CreatedCommitContributionConnection { + """ + A list of edges. + """ + edges: [CreatedCommitContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedCommitContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of commits across days and repositories in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedCommitContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedCommitContribution +} + +""" +Represents the contribution a user made on GitHub by opening an issue. +""" +type CreatedIssueContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + The issue that was opened. + """ + issue: Issue! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedIssueContribution. +""" +type CreatedIssueContributionConnection { + """ + A list of edges. + """ + edges: [CreatedIssueContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedIssueContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedIssueContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedIssueContribution +} + +""" +Represents either a issue the viewer can access or a restricted contribution. +""" +union CreatedIssueOrRestrictedContribution = CreatedIssueContribution | RestrictedContribution + +""" +Represents the contribution a user made on GitHub by opening a pull request. +""" +type CreatedPullRequestContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The pull request that was opened. + """ + pullRequest: PullRequest! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedPullRequestContribution. +""" +type CreatedPullRequestContributionConnection { + """ + A list of edges. + """ + edges: [CreatedPullRequestContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedPullRequestContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedPullRequestContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedPullRequestContribution +} + +""" +Represents either a pull request the viewer can access or a restricted contribution. +""" +union CreatedPullRequestOrRestrictedContribution = CreatedPullRequestContribution | RestrictedContribution + +""" +Represents the contribution a user made by leaving a review on a pull request. +""" +type CreatedPullRequestReviewContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The pull request the user reviewed. + """ + pullRequest: PullRequest! + + """ + The review the user left on the pull request. + """ + pullRequestReview: PullRequestReview! + + """ + The repository containing the pull request that the user reviewed. + """ + repository: Repository! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedPullRequestReviewContribution. +""" +type CreatedPullRequestReviewContributionConnection { + """ + A list of edges. + """ + edges: [CreatedPullRequestReviewContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedPullRequestReviewContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedPullRequestReviewContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedPullRequestReviewContribution +} + +""" +Represents the contribution a user made on GitHub by creating a repository. +""" +type CreatedRepositoryContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The repository that was created. + """ + repository: Repository! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedRepositoryContribution. +""" +type CreatedRepositoryContributionConnection { + """ + A list of edges. + """ + edges: [CreatedRepositoryContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedRepositoryContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedRepositoryContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedRepositoryContribution +} + +""" +Represents either a repository the viewer can access or a restricted contribution. +""" +union CreatedRepositoryOrRestrictedContribution = CreatedRepositoryContribution | RestrictedContribution + +""" +Represents a mention made by one issue or pull request to another. +""" +type CrossReferencedEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Reference originated in a different repository. + """ + isCrossRepository: Boolean! + + """ + Identifies when the reference was made. + """ + referencedAt: DateTime! + + """ + The HTTP path for this pull request. + """ + resourcePath: URI! + + """ + Issue or pull request that made the reference. + """ + source: ReferencedSubject! + + """ + Issue or pull request to which the reference was made. + """ + target: ReferencedSubject! + + """ + The HTTP URL for this pull request. + """ + url: URI! + + """ + Checks if the target will be closed when the source is merged. + """ + willCloseTarget: Boolean! +} + +""" +An ISO-8601 encoded date string. +""" +scalar Date + +""" +An ISO-8601 encoded UTC date string. +""" +scalar DateTime + +""" +Autogenerated input type of DeclineTopicSuggestion +""" +input DeclineTopicSuggestionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the suggested topic. + """ + name: String! + + """ + The reason why the suggested topic is declined. + """ + reason: TopicSuggestionDeclineReason! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of DeclineTopicSuggestion +""" +type DeclineTopicSuggestionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The declined topic. + """ + topic: Topic +} + +""" +The possible base permissions for repositories. +""" +enum DefaultRepositoryPermissionField { + """ + Can read, write, and administrate repos by default + """ + ADMIN + + """ + No access + """ + NONE + + """ + Can read repos by default + """ + READ + + """ + Can read and write repos by default + """ + WRITE +} + +""" +Entities that can be deleted. +""" +interface Deletable { + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! +} + +""" +Autogenerated input type of DeleteBranchProtectionRule +""" +input DeleteBranchProtectionRuleInput { + """ + The global relay id of the branch protection rule to be deleted. + """ + branchProtectionRuleId: ID! @possibleTypes(concreteTypes: ["BranchProtectionRule"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of DeleteBranchProtectionRule +""" +type DeleteBranchProtectionRulePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteDeployment +""" +input DeleteDeploymentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the deployment to be deleted. + """ + id: ID! @possibleTypes(concreteTypes: ["Deployment"]) +} + +""" +Autogenerated return type of DeleteDeployment +""" +type DeleteDeploymentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteDiscussionComment +""" +input DeleteDiscussionCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node id of the discussion comment to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["DiscussionComment"]) +} + +""" +Autogenerated return type of DeleteDiscussionComment +""" +type DeleteDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion comment that was just deleted. + """ + comment: DiscussionComment +} + +""" +Autogenerated input type of DeleteDiscussion +""" +input DeleteDiscussionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the discussion to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["Discussion"]) +} + +""" +Autogenerated return type of DeleteDiscussion +""" +type DeleteDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion that was just deleted. + """ + discussion: Discussion +} + +""" +Autogenerated input type of DeleteEnvironment +""" +input DeleteEnvironmentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the environment to be deleted. + """ + id: ID! @possibleTypes(concreteTypes: ["Environment"]) +} + +""" +Autogenerated return type of DeleteEnvironment +""" +type DeleteEnvironmentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteIpAllowListEntry +""" +input DeleteIpAllowListEntryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the IP allow list entry to delete. + """ + ipAllowListEntryId: ID! @possibleTypes(concreteTypes: ["IpAllowListEntry"]) +} + +""" +Autogenerated return type of DeleteIpAllowListEntry +""" +type DeleteIpAllowListEntryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list entry that was deleted. + """ + ipAllowListEntry: IpAllowListEntry +} + +""" +Autogenerated input type of DeleteIssueComment +""" +input DeleteIssueCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the comment to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["IssueComment"]) +} + +""" +Autogenerated return type of DeleteIssueComment +""" +type DeleteIssueCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteIssue +""" +input DeleteIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the issue to delete. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of DeleteIssue +""" +type DeleteIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository the issue belonged to + """ + repository: Repository +} + +""" +Autogenerated input type of DeleteLabel +""" +input DeleteLabelInput @preview(toggledBy: "bane-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the label to be deleted. + """ + id: ID! @possibleTypes(concreteTypes: ["Label"]) +} + +""" +Autogenerated return type of DeleteLabel +""" +type DeleteLabelPayload @preview(toggledBy: "bane-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteLinkedBranch +""" +input DeleteLinkedBranchInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the linked branch + """ + linkedBranchId: ID! @possibleTypes(concreteTypes: ["LinkedBranch"]) +} + +""" +Autogenerated return type of DeleteLinkedBranch +""" +type DeleteLinkedBranchPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue the linked branch was unlinked from. + """ + issue: Issue +} + +""" +Autogenerated input type of DeletePackageVersion +""" +input DeletePackageVersionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the package version to be deleted. + """ + packageVersionId: ID! @possibleTypes(concreteTypes: ["PackageVersion"]) +} + +""" +Autogenerated return type of DeletePackageVersion +""" +type DeletePackageVersionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether or not the operation succeeded. + """ + success: Boolean +} + +""" +Autogenerated input type of DeleteProjectCard +""" +input DeleteProjectCardInput { + """ + The id of the card to delete. + """ + cardId: ID! @possibleTypes(concreteTypes: ["ProjectCard"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of DeleteProjectCard +""" +type DeleteProjectCardPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The column the deleted card was in. + """ + column: ProjectColumn + + """ + The deleted card ID. + """ + deletedCardId: ID +} + +""" +Autogenerated input type of DeleteProjectColumn +""" +input DeleteProjectColumnInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the column to delete. + """ + columnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of DeleteProjectColumn +""" +type DeleteProjectColumnPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The deleted column ID. + """ + deletedColumnId: ID + + """ + The project the deleted column was in. + """ + project: Project +} + +""" +Autogenerated input type of DeleteProject +""" +input DeleteProjectInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Project ID to update. + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) +} + +""" +Autogenerated return type of DeleteProject +""" +type DeleteProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository or organization the project was removed from. + """ + owner: ProjectOwner +} + +""" +Autogenerated input type of DeleteProjectV2Field +""" +input DeleteProjectV2FieldInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the field to delete. + """ + fieldId: ID! + @possibleTypes( + concreteTypes: ["ProjectV2Field", "ProjectV2IterationField", "ProjectV2SingleSelectField"] + abstractType: "ProjectV2FieldConfiguration" + ) +} + +""" +Autogenerated return type of DeleteProjectV2Field +""" +type DeleteProjectV2FieldPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The deleted field. + """ + projectV2Field: ProjectV2FieldConfiguration +} + +""" +Autogenerated input type of DeleteProjectV2 +""" +input DeleteProjectV2Input { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project to delete. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated input type of DeleteProjectV2Item +""" +input DeleteProjectV2ItemInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the item to be removed. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the Project from which the item should be removed. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of DeleteProjectV2Item +""" +type DeleteProjectV2ItemPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the deleted item. + """ + deletedItemId: ID +} + +""" +Autogenerated return type of DeleteProjectV2 +""" +type DeleteProjectV2Payload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The deleted Project. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of DeleteProjectV2Workflow +""" +input DeleteProjectV2WorkflowInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the workflow to be removed. + """ + workflowId: ID! @possibleTypes(concreteTypes: ["ProjectV2Workflow"]) +} + +""" +Autogenerated return type of DeleteProjectV2Workflow +""" +type DeleteProjectV2WorkflowPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the deleted workflow. + """ + deletedWorkflowId: ID + + """ + The project the deleted workflow was in. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of DeletePullRequestReviewComment +""" +input DeletePullRequestReviewCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the comment to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["PullRequestReviewComment"]) +} + +""" +Autogenerated return type of DeletePullRequestReviewComment +""" +type DeletePullRequestReviewCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request review the deleted comment belonged to. + """ + pullRequestReview: PullRequestReview + + """ + The deleted pull request review comment. + """ + pullRequestReviewComment: PullRequestReviewComment +} + +""" +Autogenerated input type of DeletePullRequestReview +""" +input DeletePullRequestReviewInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pull request review to delete. + """ + pullRequestReviewId: ID! @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of DeletePullRequestReview +""" +type DeletePullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The deleted pull request review. + """ + pullRequestReview: PullRequestReview +} + +""" +Autogenerated input type of DeleteRef +""" +input DeleteRefInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the Ref to be deleted. + """ + refId: ID! @possibleTypes(concreteTypes: ["Ref"]) +} + +""" +Autogenerated return type of DeleteRef +""" +type DeleteRefPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteRepositoryRuleset +""" +input DeleteRepositoryRulesetInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The global relay id of the repository ruleset to be deleted. + """ + repositoryRulesetId: ID! @possibleTypes(concreteTypes: ["RepositoryRuleset"]) +} + +""" +Autogenerated return type of DeleteRepositoryRuleset +""" +type DeleteRepositoryRulesetPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteTeamDiscussionComment +""" +input DeleteTeamDiscussionCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the comment to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["TeamDiscussionComment"]) +} + +""" +Autogenerated return type of DeleteTeamDiscussionComment +""" +type DeleteTeamDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteTeamDiscussion +""" +input DeleteTeamDiscussionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion ID to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["TeamDiscussion"]) +} + +""" +Autogenerated return type of DeleteTeamDiscussion +""" +type DeleteTeamDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteVerifiableDomain +""" +input DeleteVerifiableDomainInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the verifiable domain to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["VerifiableDomain"]) +} + +""" +Autogenerated return type of DeleteVerifiableDomain +""" +type DeleteVerifiableDomainPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The owning account from which the domain was deleted. + """ + owner: VerifiableDomainOwner +} + +""" +Represents a 'demilestoned' event on a given issue or pull request. +""" +type DemilestonedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the milestone title associated with the 'demilestoned' event. + """ + milestoneTitle: String! + + """ + Object referenced by event. + """ + subject: MilestoneItem! +} + +""" +A Dependabot Update for a dependency in a repository +""" +type DependabotUpdate implements RepositoryNode { + """ + The error from a dependency update + """ + error: DependabotUpdateError + + """ + The associated pull request + """ + pullRequest: PullRequest + + """ + The repository associated with this node. + """ + repository: Repository! +} + +""" +An error produced from a Dependabot Update +""" +type DependabotUpdateError { + """ + The body of the error + """ + body: String! + + """ + The error code + """ + errorType: String! + + """ + The title of the error + """ + title: String! +} + +""" +A dependency manifest entry +""" +type DependencyGraphDependency @preview(toggledBy: "hawkgirl-preview") { + """ + Does the dependency itself have dependencies? + """ + hasDependencies: Boolean! + + """ + The original name of the package, as it appears in the manifest. + """ + packageLabel: String! + @deprecated( + reason: "`packageLabel` will be removed. Use normalized `packageName` field instead. Removal on 2022-10-01 UTC." + ) + + """ + The dependency package manager + """ + packageManager: String + + """ + The name of the package in the canonical form used by the package manager. + """ + packageName: String! + + """ + The repository containing the package + """ + repository: Repository + + """ + The dependency version requirements + """ + requirements: String! +} + +""" +The connection type for DependencyGraphDependency. +""" +type DependencyGraphDependencyConnection @preview(toggledBy: "hawkgirl-preview") { + """ + A list of edges. + """ + edges: [DependencyGraphDependencyEdge] + + """ + A list of nodes. + """ + nodes: [DependencyGraphDependency] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DependencyGraphDependencyEdge @preview(toggledBy: "hawkgirl-preview") { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DependencyGraphDependency +} + +""" +The possible ecosystems of a dependency graph package. +""" +enum DependencyGraphEcosystem { + """ + GitHub Actions + """ + ACTIONS + + """ + PHP packages hosted at packagist.org + """ + COMPOSER + + """ + Go modules + """ + GO + + """ + Java artifacts hosted at the Maven central repository + """ + MAVEN + + """ + JavaScript packages hosted at npmjs.com + """ + NPM + + """ + .NET packages hosted at the NuGet Gallery + """ + NUGET + + """ + Python packages hosted at PyPI.org + """ + PIP + + """ + Dart packages hosted at pub.dev + """ + PUB + + """ + Ruby gems hosted at RubyGems.org + """ + RUBYGEMS + + """ + Rust crates + """ + RUST + + """ + Swift packages + """ + SWIFT +} + +""" +Dependency manifest for a repository +""" +type DependencyGraphManifest implements Node @preview(toggledBy: "hawkgirl-preview") { + """ + Path to view the manifest file blob + """ + blobPath: String! + + """ + A list of manifest dependencies + """ + dependencies( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DependencyGraphDependencyConnection + + """ + The number of dependencies listed in the manifest + """ + dependenciesCount: Int + + """ + Is the manifest too big to parse? + """ + exceedsMaxSize: Boolean! + + """ + Fully qualified manifest filename + """ + filename: String! + id: ID! + + """ + Were we able to parse the manifest? + """ + parseable: Boolean! + + """ + The repository containing the manifest + """ + repository: Repository! +} + +""" +The connection type for DependencyGraphManifest. +""" +type DependencyGraphManifestConnection @preview(toggledBy: "hawkgirl-preview") { + """ + A list of edges. + """ + edges: [DependencyGraphManifestEdge] + + """ + A list of nodes. + """ + nodes: [DependencyGraphManifest] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DependencyGraphManifestEdge @preview(toggledBy: "hawkgirl-preview") { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DependencyGraphManifest +} + +""" +A repository deploy key. +""" +type DeployKey implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + The deploy key. + """ + key: String! + + """ + Whether or not the deploy key is read only. + """ + readOnly: Boolean! + + """ + The deploy key title. + """ + title: String! + + """ + Whether or not the deploy key has been verified. + """ + verified: Boolean! +} + +""" +The connection type for DeployKey. +""" +type DeployKeyConnection { + """ + A list of edges. + """ + edges: [DeployKeyEdge] + + """ + A list of nodes. + """ + nodes: [DeployKey] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeployKeyEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeployKey +} + +""" +Represents a 'deployed' event on a given pull request. +""" +type DeployedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The deployment associated with the 'deployed' event. + """ + deployment: Deployment! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + The ref associated with the 'deployed' event. + """ + ref: Ref +} + +""" +Represents triggered deployment instance. +""" +type Deployment implements Node { + """ + Identifies the commit sha of the deployment. + """ + commit: Commit + + """ + Identifies the oid of the deployment commit, even if the commit has been deleted. + """ + commitOid: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the actor who triggered the deployment. + """ + creator: Actor! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The deployment description. + """ + description: String + + """ + The latest environment to which this deployment was made. + """ + environment: String + id: ID! + + """ + The latest environment to which this deployment was made. + """ + latestEnvironment: String + + """ + The latest status of this deployment. + """ + latestStatus: DeploymentStatus + + """ + The original environment to which this deployment was made. + """ + originalEnvironment: String + + """ + Extra information that a deployment system might need. + """ + payload: String + + """ + Identifies the Ref of the deployment, if the deployment was created by ref. + """ + ref: Ref + + """ + Identifies the repository associated with the deployment. + """ + repository: Repository! + + """ + The current state of the deployment. + """ + state: DeploymentState + + """ + A list of statuses associated with the deployment. + """ + statuses( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentStatusConnection + + """ + The deployment task. + """ + task: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for Deployment. +""" +type DeploymentConnection { + """ + A list of edges. + """ + edges: [DeploymentEdge] + + """ + A list of nodes. + """ + nodes: [Deployment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Deployment +} + +""" +Represents a 'deployment_environment_changed' event on a given pull request. +""" +type DeploymentEnvironmentChangedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The deployment status that updated the deployment environment. + """ + deploymentStatus: DeploymentStatus! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Ordering options for deployment connections +""" +input DeploymentOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order deployments by. + """ + field: DeploymentOrderField! +} + +""" +Properties by which deployment connections can be ordered. +""" +enum DeploymentOrderField { + """ + Order collection by creation time + """ + CREATED_AT +} + +""" +A protection rule. +""" +type DeploymentProtectionRule { + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The teams or users that can review the deployment + """ + reviewers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentReviewerConnection! + + """ + The timeout in minutes for this protection rule. + """ + timeout: Int! + + """ + The type of protection rule. + """ + type: DeploymentProtectionRuleType! +} + +""" +The connection type for DeploymentProtectionRule. +""" +type DeploymentProtectionRuleConnection { + """ + A list of edges. + """ + edges: [DeploymentProtectionRuleEdge] + + """ + A list of nodes. + """ + nodes: [DeploymentProtectionRule] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentProtectionRuleEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeploymentProtectionRule +} + +""" +The possible protection rule types. +""" +enum DeploymentProtectionRuleType { + """ + Required reviewers + """ + REQUIRED_REVIEWERS + + """ + Wait timer + """ + WAIT_TIMER +} + +""" +A request to deploy a workflow run to an environment. +""" +type DeploymentRequest { + """ + Whether or not the current user can approve the deployment + """ + currentUserCanApprove: Boolean! + + """ + The target environment of the deployment + """ + environment: Environment! + + """ + The teams or users that can review the deployment + """ + reviewers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentReviewerConnection! + + """ + The wait timer in minutes configured in the environment + """ + waitTimer: Int! + + """ + The wait timer in minutes configured in the environment + """ + waitTimerStartedAt: DateTime +} + +""" +The connection type for DeploymentRequest. +""" +type DeploymentRequestConnection { + """ + A list of edges. + """ + edges: [DeploymentRequestEdge] + + """ + A list of nodes. + """ + nodes: [DeploymentRequest] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentRequestEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeploymentRequest +} + +""" +A deployment review. +""" +type DeploymentReview implements Node { + """ + The comment the user left. + """ + comment: String! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The environments approved or rejected + """ + environments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): EnvironmentConnection! + id: ID! + + """ + The decision of the user. + """ + state: DeploymentReviewState! + + """ + The user that reviewed the deployment. + """ + user: User! +} + +""" +The connection type for DeploymentReview. +""" +type DeploymentReviewConnection { + """ + A list of edges. + """ + edges: [DeploymentReviewEdge] + + """ + A list of nodes. + """ + nodes: [DeploymentReview] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentReviewEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeploymentReview +} + +""" +The possible states for a deployment review. +""" +enum DeploymentReviewState { + """ + The deployment was approved. + """ + APPROVED + + """ + The deployment was rejected. + """ + REJECTED +} + +""" +Users and teams. +""" +union DeploymentReviewer = Team | User + +""" +The connection type for DeploymentReviewer. +""" +type DeploymentReviewerConnection { + """ + A list of edges. + """ + edges: [DeploymentReviewerEdge] + + """ + A list of nodes. + """ + nodes: [DeploymentReviewer] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentReviewerEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeploymentReviewer +} + +""" +The possible states in which a deployment can be. +""" +enum DeploymentState { + """ + The pending deployment was not updated after 30 minutes. + """ + ABANDONED + + """ + The deployment is currently active. + """ + ACTIVE + + """ + An inactive transient deployment. + """ + DESTROYED + + """ + The deployment experienced an error. + """ + ERROR + + """ + The deployment has failed. + """ + FAILURE + + """ + The deployment is inactive. + """ + INACTIVE + + """ + The deployment is in progress. + """ + IN_PROGRESS + + """ + The deployment is pending. + """ + PENDING + + """ + The deployment has queued + """ + QUEUED + + """ + The deployment was successful. + """ + SUCCESS + + """ + The deployment is waiting. + """ + WAITING +} + +""" +Describes the status of a given deployment attempt. +""" +type DeploymentStatus implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the actor who triggered the deployment. + """ + creator: Actor! + + """ + Identifies the deployment associated with status. + """ + deployment: Deployment! + + """ + Identifies the description of the deployment. + """ + description: String + + """ + Identifies the environment of the deployment at the time of this deployment status + """ + environment: String @preview(toggledBy: "flash-preview") + + """ + Identifies the environment URL of the deployment. + """ + environmentUrl: URI + id: ID! + + """ + Identifies the log URL of the deployment. + """ + logUrl: URI + + """ + Identifies the current state of the deployment. + """ + state: DeploymentStatusState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for DeploymentStatus. +""" +type DeploymentStatusConnection { + """ + A list of edges. + """ + edges: [DeploymentStatusEdge] + + """ + A list of nodes. + """ + nodes: [DeploymentStatus] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentStatusEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeploymentStatus +} + +""" +The possible states for a deployment status. +""" +enum DeploymentStatusState { + """ + The deployment experienced an error. + """ + ERROR + + """ + The deployment has failed. + """ + FAILURE + + """ + The deployment is inactive. + """ + INACTIVE + + """ + The deployment is in progress. + """ + IN_PROGRESS + + """ + The deployment is pending. + """ + PENDING + + """ + The deployment is queued + """ + QUEUED + + """ + The deployment was successful. + """ + SUCCESS + + """ + The deployment is waiting. + """ + WAITING +} + +""" +Autogenerated input type of DequeuePullRequest +""" +input DequeuePullRequestInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the pull request to be dequeued. + """ + id: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of DequeuePullRequest +""" +type DequeuePullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The merge queue entry of the dequeued pull request. + """ + mergeQueueEntry: MergeQueueEntry +} + +""" +The possible sides of a diff. +""" +enum DiffSide { + """ + The left side of the diff. + """ + LEFT + + """ + The right side of the diff. + """ + RIGHT +} + +""" +Autogenerated input type of DisablePullRequestAutoMerge +""" +input DisablePullRequestAutoMergeInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to disable auto merge on. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of DisablePullRequestAutoMerge +""" +type DisablePullRequestAutoMergePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request auto merge was disabled on. + """ + pullRequest: PullRequest +} + +""" +Represents a 'disconnected' event on a given issue or pull request. +""" +type DisconnectedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Reference originated in a different repository. + """ + isCrossRepository: Boolean! + + """ + Issue or pull request from which the issue was disconnected. + """ + source: ReferencedSubject! + + """ + Issue or pull request which was disconnected. + """ + subject: ReferencedSubject! +} + +""" +A discussion in a repository. +""" +type Discussion implements Closable & Comment & Deletable & Labelable & Lockable & Node & Reactable & RepositoryNode & Subscribable & Updatable & Votable { + """ + Reason that the conversation was locked. + """ + activeLockReason: LockReason + + """ + The comment chosen as this discussion's answer, if any. + """ + answer: DiscussionComment + + """ + The time when a user chose this discussion's answer, if answered. + """ + answerChosenAt: DateTime + + """ + The user who chose this discussion's answer, if answered. + """ + answerChosenBy: Actor + + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The main text of the discussion post. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + The category for this discussion. + """ + category: DiscussionCategory! + + """ + Indicates if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + The replies to the discussion. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DiscussionCommentConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + A list of labels associated with the object. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + `true` if the object is locked + """ + locked: Boolean! + + """ + The number identifying this discussion within the repository. + """ + number: Int! + + """ + The poll associated with this discussion, if one exists. + """ + poll: DiscussionPoll + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The path for this discussion. + """ + resourcePath: URI! + + """ + Identifies the reason for the discussion's state. + """ + stateReason: DiscussionStateReason + + """ + The title of this discussion. + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + Number of upvotes that this subject has received. + """ + upvoteCount: Int! + + """ + The URL for this discussion. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Whether or not the current user can add or remove an upvote on this subject. + """ + viewerCanUpvote: Boolean! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + Whether or not the current user has already upvoted this subject. + """ + viewerHasUpvoted: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +A category for discussions in a repository. +""" +type DiscussionCategory implements Node & RepositoryNode { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + A description of this category. + """ + description: String + + """ + An emoji representing this category. + """ + emoji: String! + + """ + This category's emoji rendered as HTML. + """ + emojiHTML: HTML! + id: ID! + + """ + Whether or not discussions in this category support choosing an answer with the markDiscussionCommentAsAnswer mutation. + """ + isAnswerable: Boolean! + + """ + The name of this category. + """ + name: String! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The slug of this category. + """ + slug: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for DiscussionCategory. +""" +type DiscussionCategoryConnection { + """ + A list of edges. + """ + edges: [DiscussionCategoryEdge] + + """ + A list of nodes. + """ + nodes: [DiscussionCategory] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DiscussionCategoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DiscussionCategory +} + +""" +The possible reasons for closing a discussion. +""" +enum DiscussionCloseReason { + """ + The discussion is a duplicate of another + """ + DUPLICATE + + """ + The discussion is no longer relevant + """ + OUTDATED + + """ + The discussion has been resolved + """ + RESOLVED +} + +""" +A comment on a discussion. +""" +type DiscussionComment implements Comment & Deletable & Minimizable & Node & Reactable & Updatable & UpdatableComment & Votable { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The time when this replied-to comment was deleted + """ + deletedAt: DateTime + + """ + The discussion this comment was created in + """ + discussion: Discussion + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Has this comment been chosen as the answer of its discussion? + """ + isAnswer: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The threaded replies to this comment. + """ + replies( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DiscussionCommentConnection! + + """ + The discussion comment this comment is a reply to + """ + replyTo: DiscussionComment + + """ + The path for this discussion comment. + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + Number of upvotes that this subject has received. + """ + upvoteCount: Int! + + """ + The URL for this discussion comment. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Can the current user mark this comment as an answer? + """ + viewerCanMarkAsAnswer: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Can the current user unmark this comment as an answer? + """ + viewerCanUnmarkAsAnswer: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Whether or not the current user can add or remove an upvote on this subject. + """ + viewerCanUpvote: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + Whether or not the current user has already upvoted this subject. + """ + viewerHasUpvoted: Boolean! +} + +""" +The connection type for DiscussionComment. +""" +type DiscussionCommentConnection { + """ + A list of edges. + """ + edges: [DiscussionCommentEdge] + + """ + A list of nodes. + """ + nodes: [DiscussionComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DiscussionCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DiscussionComment +} + +""" +The connection type for Discussion. +""" +type DiscussionConnection { + """ + A list of edges. + """ + edges: [DiscussionEdge] + + """ + A list of nodes. + """ + nodes: [Discussion] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DiscussionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Discussion +} + +""" +Ways in which lists of discussions can be ordered upon return. +""" +input DiscussionOrder { + """ + The direction in which to order discussions by the specified field. + """ + direction: OrderDirection! + + """ + The field by which to order discussions. + """ + field: DiscussionOrderField! +} + +""" +Properties by which discussion connections can be ordered. +""" +enum DiscussionOrderField { + """ + Order discussions by creation time. + """ + CREATED_AT + + """ + Order discussions by most recent modification time. + """ + UPDATED_AT +} + +""" +A poll for a discussion. +""" +type DiscussionPoll implements Node { + """ + The discussion that this poll belongs to. + """ + discussion: Discussion + id: ID! + + """ + The options for this poll. + """ + options( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + How to order the options for the discussion poll. + """ + orderBy: DiscussionPollOptionOrder = {field: AUTHORED_ORDER, direction: ASC} + ): DiscussionPollOptionConnection + + """ + The question that is being asked by this poll. + """ + question: String! + + """ + The total number of votes that have been cast for this poll. + """ + totalVoteCount: Int! + + """ + Indicates if the viewer has permission to vote in this poll. + """ + viewerCanVote: Boolean! + + """ + Indicates if the viewer has voted for any option in this poll. + """ + viewerHasVoted: Boolean! +} + +""" +An option for a discussion poll. +""" +type DiscussionPollOption implements Node { + id: ID! + + """ + The text for this option. + """ + option: String! + + """ + The discussion poll that this option belongs to. + """ + poll: DiscussionPoll + + """ + The total number of votes that have been cast for this option. + """ + totalVoteCount: Int! + + """ + Indicates if the viewer has voted for this option in the poll. + """ + viewerHasVoted: Boolean! +} + +""" +The connection type for DiscussionPollOption. +""" +type DiscussionPollOptionConnection { + """ + A list of edges. + """ + edges: [DiscussionPollOptionEdge] + + """ + A list of nodes. + """ + nodes: [DiscussionPollOption] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DiscussionPollOptionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DiscussionPollOption +} + +""" +Ordering options for discussion poll option connections. +""" +input DiscussionPollOptionOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order poll options by. + """ + field: DiscussionPollOptionOrderField! +} + +""" +Properties by which discussion poll option connections can be ordered. +""" +enum DiscussionPollOptionOrderField { + """ + Order poll options by the order that the poll author specified when creating the poll. + """ + AUTHORED_ORDER + + """ + Order poll options by the number of votes it has. + """ + VOTE_COUNT +} + +""" +The possible states of a discussion. +""" +enum DiscussionState { + """ + A discussion that has been closed + """ + CLOSED + + """ + A discussion that is open + """ + OPEN +} + +""" +The possible state reasons of a discussion. +""" +enum DiscussionStateReason { + """ + The discussion is a duplicate of another + """ + DUPLICATE + + """ + The discussion is no longer relevant + """ + OUTDATED + + """ + The discussion was reopened + """ + REOPENED + + """ + The discussion has been resolved + """ + RESOLVED +} + +""" +Autogenerated input type of DismissPullRequestReview +""" +input DismissPullRequestReviewInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The contents of the pull request review dismissal message. + """ + message: String! + + """ + The Node ID of the pull request review to modify. + """ + pullRequestReviewId: ID! @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of DismissPullRequestReview +""" +type DismissPullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The dismissed pull request review. + """ + pullRequestReview: PullRequestReview +} + +""" +The possible reasons that a Dependabot alert was dismissed. +""" +enum DismissReason { + """ + A fix has already been started + """ + FIX_STARTED + + """ + This alert is inaccurate or incorrect + """ + INACCURATE + + """ + Vulnerable code is not actually used + """ + NOT_USED + + """ + No bandwidth to fix this + """ + NO_BANDWIDTH + + """ + Risk is tolerable to this project + """ + TOLERABLE_RISK +} + +""" +Autogenerated input type of DismissRepositoryVulnerabilityAlert +""" +input DismissRepositoryVulnerabilityAlertInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The reason the Dependabot alert is being dismissed. + """ + dismissReason: DismissReason! + + """ + The Dependabot alert ID to dismiss. + """ + repositoryVulnerabilityAlertId: ID! @possibleTypes(concreteTypes: ["RepositoryVulnerabilityAlert"]) +} + +""" +Autogenerated return type of DismissRepositoryVulnerabilityAlert +""" +type DismissRepositoryVulnerabilityAlertPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Dependabot alert that was dismissed + """ + repositoryVulnerabilityAlert: RepositoryVulnerabilityAlert +} + +""" +A draft issue within a project. +""" +type DraftIssue implements Node { + """ + A list of users to assigned to this draft issue. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The body of the draft issue. + """ + body: String! + + """ + The body of the draft issue rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body of the draft issue rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created this draft issue. + """ + creator: Actor + id: ID! + + """ + List of items linked with the draft issue (currently draft issue can be linked to only one item). + """ + projectV2Items( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2ItemConnection! + + """ + Projects that link to this draft issue (currently draft issue can be linked to only one project). + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2Connection! + + """ + The title of the draft issue + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +Specifies a review comment to be left with a Pull Request Review. +""" +input DraftPullRequestReviewComment { + """ + Body of the comment to leave. + """ + body: String! + + """ + Path to the file being commented on. + """ + path: String! + + """ + Position in the file to leave a comment on. + """ + position: Int! +} + +""" +Specifies a review comment thread to be left with a Pull Request Review. +""" +input DraftPullRequestReviewThread { + """ + Body of the comment to leave. + """ + body: String! + + """ + The line of the blob to which the thread refers. The end of the line range for multi-line comments. + """ + line: Int! + + """ + Path to the file being commented on. + """ + path: String! + + """ + The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range. + """ + side: DiffSide = RIGHT + + """ + The first line of the range to which the comment refers. + """ + startLine: Int + + """ + The side of the diff on which the start line resides. + """ + startSide: DiffSide = RIGHT +} + +""" +Autogenerated input type of EnablePullRequestAutoMerge +""" +input EnablePullRequestAutoMergeInput { + """ + The email address to associate with this merge. + """ + authorEmail: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Commit body to use for the commit when the PR is mergable; if omitted, a + default message will be used. NOTE: when merging with a merge queue any input + value for commit message is ignored. + """ + commitBody: String + + """ + Commit headline to use for the commit when the PR is mergable; if omitted, a + default message will be used. NOTE: when merging with a merge queue any input + value for commit headline is ignored. + """ + commitHeadline: String + + """ + The expected head OID of the pull request. + """ + expectedHeadOid: GitObjectID + + """ + The merge method to use. If omitted, defaults to `MERGE`. NOTE: when merging + with a merge queue any input value for merge method is ignored. + """ + mergeMethod: PullRequestMergeMethod = MERGE + + """ + ID of the pull request to enable auto-merge on. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of EnablePullRequestAutoMerge +""" +type EnablePullRequestAutoMergePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request auto-merge was enabled on. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of EnqueuePullRequest +""" +input EnqueuePullRequestInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The expected head OID of the pull request. + """ + expectedHeadOid: GitObjectID + + """ + Add the pull request to the front of the queue. + """ + jump: Boolean + + """ + The ID of the pull request to enqueue. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of EnqueuePullRequest +""" +type EnqueuePullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The merge queue entry for the enqueued pull request. + """ + mergeQueueEntry: MergeQueueEntry +} + +""" +An account to manage multiple organizations with consolidated policy and billing. +""" +type Enterprise implements AnnouncementBanner & Node { + """ + The text of the announcement + """ + announcement: String + + """ + The expiration date of the announcement, if any + """ + announcementExpiresAt: DateTime + + """ + Whether the announcement can be dismissed by the user + """ + announcementUserDismissible: Boolean + + """ + A URL pointing to the enterprise's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + Enterprise billing information visible to enterprise billing managers. + """ + billingInfo: EnterpriseBillingInfo + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The description of the enterprise. + """ + description: String + + """ + The description of the enterprise as HTML. + """ + descriptionHTML: HTML! + id: ID! + + """ + The location of the enterprise. + """ + location: String + + """ + A list of users who are members of this enterprise. + """ + members( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Only return members within the selected GitHub Enterprise deployment + """ + deployment: EnterpriseUserDeployment + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Only return members with this two-factor authentication status. Does not + include members who only have an account on a GitHub Enterprise Server instance. + """ + hasTwoFactorEnabled: Boolean = null + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for members returned from the connection. + """ + orderBy: EnterpriseMemberOrder = {field: LOGIN, direction: ASC} + + """ + Only return members within the organizations with these logins + """ + organizationLogins: [String!] + + """ + The search string to look for. + """ + query: String + + """ + The role of the user in the enterprise organization or server. + """ + role: EnterpriseUserAccountMembershipRole + ): EnterpriseMemberConnection! + + """ + The name of the enterprise. + """ + name: String! + + """ + A list of organizations that belong to this enterprise. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations returned from the connection. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The search string to look for. + """ + query: String + + """ + The viewer's role in an organization. + """ + viewerOrganizationRole: RoleInOrganization + ): OrganizationConnection! + + """ + Enterprise information visible to enterprise owners or enterprise owners' + personal access tokens (classic) with read:enterprise or admin:enterprise scope. + """ + ownerInfo: EnterpriseOwnerInfo + + """ + The HTTP path for this enterprise. + """ + resourcePath: URI! + + """ + The URL-friendly identifier for the enterprise. + """ + slug: String! + + """ + The HTTP URL for this enterprise. + """ + url: URI! + + """ + Is the current viewer an admin of this enterprise? + """ + viewerIsAdmin: Boolean! + + """ + The URL of the enterprise website. + """ + websiteUrl: URI +} + +""" +The connection type for User. +""" +type EnterpriseAdministratorConnection { + """ + A list of edges. + """ + edges: [EnterpriseAdministratorEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A User who is an administrator of an enterprise. +""" +type EnterpriseAdministratorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: User + + """ + The role of the administrator. + """ + role: EnterpriseAdministratorRole! +} + +""" +An invitation for a user to become an owner or billing manager of an enterprise. +""" +type EnterpriseAdministratorInvitation implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The email of the person who was invited to the enterprise. + """ + email: String + + """ + The enterprise the invitation is for. + """ + enterprise: Enterprise! + id: ID! + + """ + The user who was invited to the enterprise. + """ + invitee: User + + """ + The user who created the invitation. + """ + inviter: User + + """ + The invitee's pending role in the enterprise (owner or billing_manager). + """ + role: EnterpriseAdministratorRole! +} + +""" +The connection type for EnterpriseAdministratorInvitation. +""" +type EnterpriseAdministratorInvitationConnection { + """ + A list of edges. + """ + edges: [EnterpriseAdministratorInvitationEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseAdministratorInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseAdministratorInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseAdministratorInvitation +} + +""" +Ordering options for enterprise administrator invitation connections +""" +input EnterpriseAdministratorInvitationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order enterprise administrator invitations by. + """ + field: EnterpriseAdministratorInvitationOrderField! +} + +""" +Properties by which enterprise administrator invitation connections can be ordered. +""" +enum EnterpriseAdministratorInvitationOrderField { + """ + Order enterprise administrator member invitations by creation time + """ + CREATED_AT +} + +""" +The possible administrator roles in an enterprise account. +""" +enum EnterpriseAdministratorRole { + """ + Represents a billing manager of the enterprise account. + """ + BILLING_MANAGER + + """ + Represents an owner of the enterprise account. + """ + OWNER +} + +""" +The possible values for the enterprise allow private repository forking policy value. +""" +enum EnterpriseAllowPrivateRepositoryForkingPolicyValue { + """ + Members can fork a repository to an organization within this enterprise. + """ + ENTERPRISE_ORGANIZATIONS + + """ + Members can fork a repository to their enterprise-managed user account or an organization inside this enterprise. + """ + ENTERPRISE_ORGANIZATIONS_USER_ACCOUNTS + + """ + Members can fork a repository to their user account or an organization, either inside or outside of this enterprise. + """ + EVERYWHERE + + """ + Members can fork a repository only within the same organization (intra-org). + """ + SAME_ORGANIZATION + + """ + Members can fork a repository to their user account or within the same organization. + """ + SAME_ORGANIZATION_USER_ACCOUNTS + + """ + Members can fork a repository to their user account. + """ + USER_ACCOUNTS +} + +""" +Metadata for an audit entry containing enterprise account information. +""" +interface EnterpriseAuditEntryData { + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI +} + +""" +Enterprise billing information visible to enterprise billing managers and owners. +""" +type EnterpriseBillingInfo { + """ + The number of licenseable users/emails across the enterprise. + """ + allLicensableUsersCount: Int! + + """ + The number of data packs used by all organizations owned by the enterprise. + """ + assetPacks: Int! + + """ + The bandwidth quota in GB for all organizations owned by the enterprise. + """ + bandwidthQuota: Float! + + """ + The bandwidth usage in GB for all organizations owned by the enterprise. + """ + bandwidthUsage: Float! + + """ + The bandwidth usage as a percentage of the bandwidth quota. + """ + bandwidthUsagePercentage: Int! + + """ + The storage quota in GB for all organizations owned by the enterprise. + """ + storageQuota: Float! + + """ + The storage usage in GB for all organizations owned by the enterprise. + """ + storageUsage: Float! + + """ + The storage usage as a percentage of the storage quota. + """ + storageUsagePercentage: Int! + + """ + The number of available licenses across all owned organizations based on the unique number of billable users. + """ + totalAvailableLicenses: Int! + + """ + The total number of licenses allocated. + """ + totalLicenses: Int! +} + +""" +The possible values for the enterprise base repository permission setting. +""" +enum EnterpriseDefaultRepositoryPermissionSettingValue { + """ + Organization members will be able to clone, pull, push, and add new collaborators to all organization repositories. + """ + ADMIN + + """ + Organization members will only be able to clone and pull public repositories. + """ + NONE + + """ + Organizations in the enterprise choose base repository permissions for their members. + """ + NO_POLICY + + """ + Organization members will be able to clone and pull all organization repositories. + """ + READ + + """ + Organization members will be able to clone, pull, and push all organization repositories. + """ + WRITE +} + +""" +The possible values for an enabled/disabled enterprise setting. +""" +enum EnterpriseEnabledDisabledSettingValue { + """ + The setting is disabled for organizations in the enterprise. + """ + DISABLED + + """ + The setting is enabled for organizations in the enterprise. + """ + ENABLED + + """ + There is no policy set for organizations in the enterprise. + """ + NO_POLICY +} + +""" +The possible values for an enabled/no policy enterprise setting. +""" +enum EnterpriseEnabledSettingValue { + """ + The setting is enabled for organizations in the enterprise. + """ + ENABLED + + """ + There is no policy set for organizations in the enterprise. + """ + NO_POLICY +} + +""" +The connection type for OrganizationInvitation. +""" +type EnterpriseFailedInvitationConnection { + """ + A list of edges. + """ + edges: [EnterpriseFailedInvitationEdge] + + """ + A list of nodes. + """ + nodes: [OrganizationInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Identifies the total count of unique users in the connection. + """ + totalUniqueUserCount: Int! +} + +""" +A failed invitation to be a member in an enterprise organization. +""" +type EnterpriseFailedInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: OrganizationInvitation +} + +""" +An identity provider configured to provision identities for an enterprise. +Visible to enterprise owners or enterprise owners' personal access tokens +(classic) with read:enterprise or admin:enterprise scope. +""" +type EnterpriseIdentityProvider implements Node { + """ + The digest algorithm used to sign SAML requests for the identity provider. + """ + digestMethod: SamlDigestAlgorithm + + """ + The enterprise this identity provider belongs to. + """ + enterprise: Enterprise + + """ + ExternalIdentities provisioned by this identity provider. + """ + externalIdentities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter to external identities with the users login + """ + login: String + + """ + Filter to external identities with valid org membership only + """ + membersOnly: Boolean + + """ + Filter to external identities with the users userName/NameID attribute + """ + userName: String + ): ExternalIdentityConnection! + id: ID! + + """ + The x509 certificate used by the identity provider to sign assertions and responses. + """ + idpCertificate: X509Certificate + + """ + The Issuer Entity ID for the SAML identity provider. + """ + issuer: String + + """ + Recovery codes that can be used by admins to access the enterprise if the identity provider is unavailable. + """ + recoveryCodes: [String!] + + """ + The signature algorithm used to sign SAML requests for the identity provider. + """ + signatureMethod: SamlSignatureAlgorithm + + """ + The URL endpoint for the identity provider's SAML SSO. + """ + ssoUrl: URI +} + +""" +An object that is a member of an enterprise. +""" +union EnterpriseMember = EnterpriseUserAccount | User + +""" +The connection type for EnterpriseMember. +""" +type EnterpriseMemberConnection { + """ + A list of edges. + """ + edges: [EnterpriseMemberEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseMember] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A User who is a member of an enterprise through one or more organizations. +""" +type EnterpriseMemberEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseMember +} + +""" +Ordering options for enterprise member connections. +""" +input EnterpriseMemberOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order enterprise members by. + """ + field: EnterpriseMemberOrderField! +} + +""" +Properties by which enterprise member connections can be ordered. +""" +enum EnterpriseMemberOrderField { + """ + Order enterprise members by creation time + """ + CREATED_AT + + """ + Order enterprise members by login + """ + LOGIN +} + +""" +The possible values for the enterprise members can create repositories setting. +""" +enum EnterpriseMembersCanCreateRepositoriesSettingValue { + """ + Members will be able to create public and private repositories. + """ + ALL + + """ + Members will not be able to create public or private repositories. + """ + DISABLED + + """ + Organization administrators choose whether to allow members to create repositories. + """ + NO_POLICY + + """ + Members will be able to create only private repositories. + """ + PRIVATE + + """ + Members will be able to create only public repositories. + """ + PUBLIC +} + +""" +The possible values for the members can make purchases setting. +""" +enum EnterpriseMembersCanMakePurchasesSettingValue { + """ + The setting is disabled for organizations in the enterprise. + """ + DISABLED + + """ + The setting is enabled for organizations in the enterprise. + """ + ENABLED +} + +""" +The connection type for Organization. +""" +type EnterpriseOrganizationMembershipConnection { + """ + A list of edges. + """ + edges: [EnterpriseOrganizationMembershipEdge] + + """ + A list of nodes. + """ + nodes: [Organization] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An enterprise organization that a user is a member of. +""" +type EnterpriseOrganizationMembershipEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Organization + + """ + The role of the user in the enterprise membership. + """ + role: EnterpriseUserAccountMembershipRole! +} + +""" +The connection type for User. +""" +type EnterpriseOutsideCollaboratorConnection { + """ + A list of edges. + """ + edges: [EnterpriseOutsideCollaboratorEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A User who is an outside collaborator of an enterprise through one or more organizations. +""" +type EnterpriseOutsideCollaboratorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: User + + """ + The enterprise organization repositories this user is a member of. + """ + repositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories. + """ + orderBy: RepositoryOrder = {field: NAME, direction: ASC} + ): EnterpriseRepositoryInfoConnection! +} + +""" +Enterprise information visible to enterprise owners or enterprise owners' +personal access tokens (classic) with read:enterprise or admin:enterprise scope. +""" +type EnterpriseOwnerInfo { + """ + A list of all of the administrators for this enterprise. + """ + admins( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Only return administrators with this two-factor authentication status. + """ + hasTwoFactorEnabled: Boolean = null + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for administrators returned from the connection. + """ + orderBy: EnterpriseMemberOrder = {field: LOGIN, direction: ASC} + + """ + Only return members within the organizations with these logins + """ + organizationLogins: [String!] + + """ + The search string to look for. + """ + query: String + + """ + The role to filter by. + """ + role: EnterpriseAdministratorRole + ): EnterpriseAdministratorConnection! + + """ + A list of users in the enterprise who currently have two-factor authentication disabled. + """ + affiliatedUsersWithTwoFactorDisabled( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + Whether or not affiliated users with two-factor authentication disabled exist in the enterprise. + """ + affiliatedUsersWithTwoFactorDisabledExist: Boolean! + + """ + The setting value for whether private repository forking is enabled for repositories in organizations in this enterprise. + """ + allowPrivateRepositoryForkingSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided private repository forking setting value. + """ + allowPrivateRepositoryForkingSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The value for the allow private repository forking policy on the enterprise. + """ + allowPrivateRepositoryForkingSettingPolicyValue: EnterpriseAllowPrivateRepositoryForkingPolicyValue + + """ + The setting value for base repository permissions for organizations in this enterprise. + """ + defaultRepositoryPermissionSetting: EnterpriseDefaultRepositoryPermissionSettingValue! + + """ + A list of enterprise organizations configured with the provided base repository permission. + """ + defaultRepositoryPermissionSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The permission to find organizations for. + """ + value: DefaultRepositoryPermissionField! + ): OrganizationConnection! + + """ + A list of domains owned by the enterprise. Visible to enterprise owners or + enterprise owners' personal access tokens (classic) with admin:enterprise scope. + """ + domains( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter whether or not the domain is approved. + """ + isApproved: Boolean = null + + """ + Filter whether or not the domain is verified. + """ + isVerified: Boolean = null + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for verifiable domains returned. + """ + orderBy: VerifiableDomainOrder = {field: DOMAIN, direction: ASC} + ): VerifiableDomainConnection! + + """ + Enterprise Server installations owned by the enterprise. + """ + enterpriseServerInstallations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Whether or not to only return installations discovered via GitHub Connect. + """ + connectedOnly: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Enterprise Server installations returned. + """ + orderBy: EnterpriseServerInstallationOrder = {field: HOST_NAME, direction: ASC} + ): EnterpriseServerInstallationConnection! + + """ + A list of failed invitations in the enterprise. + """ + failedInvitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The search string to look for. + """ + query: String + ): EnterpriseFailedInvitationConnection! + + """ + The setting value for whether the enterprise has an IP allow list enabled. + """ + ipAllowListEnabledSetting: IpAllowListEnabledSettingValue! + + """ + The IP addresses that are allowed to access resources owned by the enterprise. + Visible to enterprise owners or enterprise owners' personal access tokens + (classic) with admin:enterprise scope. + """ + ipAllowListEntries( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for IP allow list entries returned. + """ + orderBy: IpAllowListEntryOrder = {field: ALLOW_LIST_VALUE, direction: ASC} + ): IpAllowListEntryConnection! + + """ + The setting value for whether the enterprise has IP allow list configuration for installed GitHub Apps enabled. + """ + ipAllowListForInstalledAppsEnabledSetting: IpAllowListForInstalledAppsEnabledSettingValue! + + """ + Whether or not the base repository permission is currently being updated. + """ + isUpdatingDefaultRepositoryPermission: Boolean! + + """ + Whether the two-factor authentication requirement is currently being enforced. + """ + isUpdatingTwoFactorRequirement: Boolean! + + """ + The setting value for whether organization members with admin permissions on a + repository can change repository visibility. + """ + membersCanChangeRepositoryVisibilitySetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided can change repository visibility setting value. + """ + membersCanChangeRepositoryVisibilitySettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether members of organizations in the enterprise can create internal repositories. + """ + membersCanCreateInternalRepositoriesSetting: Boolean + + """ + The setting value for whether members of organizations in the enterprise can create private repositories. + """ + membersCanCreatePrivateRepositoriesSetting: Boolean + + """ + The setting value for whether members of organizations in the enterprise can create public repositories. + """ + membersCanCreatePublicRepositoriesSetting: Boolean + + """ + The setting value for whether members of organizations in the enterprise can create repositories. + """ + membersCanCreateRepositoriesSetting: EnterpriseMembersCanCreateRepositoriesSettingValue + + """ + A list of enterprise organizations configured with the provided repository creation setting value. + """ + membersCanCreateRepositoriesSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting to find organizations for. + """ + value: OrganizationMembersCanCreateRepositoriesSettingValue! + ): OrganizationConnection! + + """ + The setting value for whether members with admin permissions for repositories can delete issues. + """ + membersCanDeleteIssuesSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can delete issues setting value. + """ + membersCanDeleteIssuesSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether members with admin permissions for repositories can delete or transfer repositories. + """ + membersCanDeleteRepositoriesSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can delete repositories setting value. + """ + membersCanDeleteRepositoriesSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether members of organizations in the enterprise can invite outside collaborators. + """ + membersCanInviteCollaboratorsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can invite collaborators setting value. + """ + membersCanInviteCollaboratorsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + Indicates whether members of this enterprise's organizations can purchase additional services for those organizations. + """ + membersCanMakePurchasesSetting: EnterpriseMembersCanMakePurchasesSettingValue! + + """ + The setting value for whether members with admin permissions for repositories can update protected branches. + """ + membersCanUpdateProtectedBranchesSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can update protected branches setting value. + """ + membersCanUpdateProtectedBranchesSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether members can view dependency insights. + """ + membersCanViewDependencyInsightsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can view dependency insights setting value. + """ + membersCanViewDependencyInsightsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + Indicates if email notification delivery for this enterprise is restricted to verified or approved domains. + """ + notificationDeliveryRestrictionEnabledSetting: NotificationRestrictionSettingValue! + + """ + The OIDC Identity Provider for the enterprise. + """ + oidcProvider: OIDCProvider + + """ + The setting value for whether organization projects are enabled for organizations in this enterprise. + """ + organizationProjectsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided organization projects setting value. + """ + organizationProjectsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + A list of outside collaborators across the repositories in the enterprise. + """ + outsideCollaborators( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Only return outside collaborators with this two-factor authentication status. + """ + hasTwoFactorEnabled: Boolean = null + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The login of one specific outside collaborator. + """ + login: String + + """ + Ordering options for outside collaborators returned from the connection. + """ + orderBy: EnterpriseMemberOrder = {field: LOGIN, direction: ASC} + + """ + Only return outside collaborators within the organizations with these logins + """ + organizationLogins: [String!] + + """ + The search string to look for. + """ + query: String + + """ + Only return outside collaborators on repositories with this visibility. + """ + visibility: RepositoryVisibility + ): EnterpriseOutsideCollaboratorConnection! + + """ + A list of pending administrator invitations for the enterprise. + """ + pendingAdminInvitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pending enterprise administrator invitations returned from the connection. + """ + orderBy: EnterpriseAdministratorInvitationOrder = {field: CREATED_AT, direction: DESC} + + """ + The search string to look for. + """ + query: String + + """ + The role to filter by. + """ + role: EnterpriseAdministratorRole + ): EnterpriseAdministratorInvitationConnection! + + """ + A list of pending collaborator invitations across the repositories in the enterprise. + """ + pendingCollaboratorInvitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pending repository collaborator invitations returned from the connection. + """ + orderBy: RepositoryInvitationOrder = {field: CREATED_AT, direction: DESC} + + """ + The search string to look for. + """ + query: String + ): RepositoryInvitationConnection! + + """ + A list of pending member invitations for organizations in the enterprise. + """ + pendingMemberInvitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Only return invitations matching this invitation source + """ + invitationSource: OrganizationInvitationSource + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Only return invitations within the organizations with these logins + """ + organizationLogins: [String!] + + """ + The search string to look for. + """ + query: String + ): EnterprisePendingMemberInvitationConnection! + + """ + The setting value for whether repository projects are enabled in this enterprise. + """ + repositoryProjectsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided repository projects setting value. + """ + repositoryProjectsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The SAML Identity Provider for the enterprise. + """ + samlIdentityProvider: EnterpriseIdentityProvider + + """ + A list of enterprise organizations configured with the SAML single sign-on setting value. + """ + samlIdentityProviderSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: IdentityProviderConfigurationState! + ): OrganizationConnection! + + """ + A list of members with a support entitlement. + """ + supportEntitlements( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for support entitlement users returned from the connection. + """ + orderBy: EnterpriseMemberOrder = {field: LOGIN, direction: ASC} + ): EnterpriseMemberConnection! + + """ + The setting value for whether team discussions are enabled for organizations in this enterprise. + """ + teamDiscussionsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided team discussions setting value. + """ + teamDiscussionsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether the enterprise requires two-factor authentication for its organizations and users. + """ + twoFactorRequiredSetting: EnterpriseEnabledSettingValue! + + """ + A list of enterprise organizations configured with the two-factor authentication setting value. + """ + twoFactorRequiredSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! +} + +""" +The connection type for OrganizationInvitation. +""" +type EnterprisePendingMemberInvitationConnection { + """ + A list of edges. + """ + edges: [EnterprisePendingMemberInvitationEdge] + + """ + A list of nodes. + """ + nodes: [OrganizationInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Identifies the total count of unique users in the connection. + """ + totalUniqueUserCount: Int! +} + +""" +An invitation to be a member in an enterprise organization. +""" +type EnterprisePendingMemberInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: OrganizationInvitation +} + +""" +A subset of repository information queryable from an enterprise. +""" +type EnterpriseRepositoryInfo implements Node { + id: ID! + + """ + Identifies if the repository is private or internal. + """ + isPrivate: Boolean! + + """ + The repository's name. + """ + name: String! + + """ + The repository's name with owner. + """ + nameWithOwner: String! +} + +""" +The connection type for EnterpriseRepositoryInfo. +""" +type EnterpriseRepositoryInfoConnection { + """ + A list of edges. + """ + edges: [EnterpriseRepositoryInfoEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseRepositoryInfo] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseRepositoryInfoEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseRepositoryInfo +} + +""" +An Enterprise Server installation. +""" +type EnterpriseServerInstallation implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The customer name to which the Enterprise Server installation belongs. + """ + customerName: String! + + """ + The host name of the Enterprise Server installation. + """ + hostName: String! + id: ID! + + """ + Whether or not the installation is connected to an Enterprise Server installation via GitHub Connect. + """ + isConnected: Boolean! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + User accounts on this Enterprise Server installation. + """ + userAccounts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Enterprise Server user accounts returned from the connection. + """ + orderBy: EnterpriseServerUserAccountOrder = {field: LOGIN, direction: ASC} + ): EnterpriseServerUserAccountConnection! + + """ + User accounts uploads for the Enterprise Server installation. + """ + userAccountsUploads( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Enterprise Server user accounts uploads returned from the connection. + """ + orderBy: EnterpriseServerUserAccountsUploadOrder = {field: CREATED_AT, direction: DESC} + ): EnterpriseServerUserAccountsUploadConnection! +} + +""" +The connection type for EnterpriseServerInstallation. +""" +type EnterpriseServerInstallationConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerInstallationEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerInstallation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseServerInstallationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerInstallation +} + +""" +The connection type for EnterpriseServerInstallation. +""" +type EnterpriseServerInstallationMembershipConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerInstallationMembershipEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerInstallation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An Enterprise Server installation that a user is a member of. +""" +type EnterpriseServerInstallationMembershipEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerInstallation + + """ + The role of the user in the enterprise membership. + """ + role: EnterpriseUserAccountMembershipRole! +} + +""" +Ordering options for Enterprise Server installation connections. +""" +input EnterpriseServerInstallationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order Enterprise Server installations by. + """ + field: EnterpriseServerInstallationOrderField! +} + +""" +Properties by which Enterprise Server installation connections can be ordered. +""" +enum EnterpriseServerInstallationOrderField { + """ + Order Enterprise Server installations by creation time + """ + CREATED_AT + + """ + Order Enterprise Server installations by customer name + """ + CUSTOMER_NAME + + """ + Order Enterprise Server installations by host name + """ + HOST_NAME +} + +""" +A user account on an Enterprise Server installation. +""" +type EnterpriseServerUserAccount implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + User emails belonging to this user account. + """ + emails( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Enterprise Server user account emails returned from the connection. + """ + orderBy: EnterpriseServerUserAccountEmailOrder = {field: EMAIL, direction: ASC} + ): EnterpriseServerUserAccountEmailConnection! + + """ + The Enterprise Server installation on which this user account exists. + """ + enterpriseServerInstallation: EnterpriseServerInstallation! + id: ID! + + """ + Whether the user account is a site administrator on the Enterprise Server installation. + """ + isSiteAdmin: Boolean! + + """ + The login of the user account on the Enterprise Server installation. + """ + login: String! + + """ + The profile name of the user account on the Enterprise Server installation. + """ + profileName: String + + """ + The date and time when the user account was created on the Enterprise Server installation. + """ + remoteCreatedAt: DateTime! + + """ + The ID of the user account on the Enterprise Server installation. + """ + remoteUserId: Int! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for EnterpriseServerUserAccount. +""" +type EnterpriseServerUserAccountConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerUserAccountEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerUserAccount] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseServerUserAccountEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerUserAccount +} + +""" +An email belonging to a user account on an Enterprise Server installation. +""" +type EnterpriseServerUserAccountEmail implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The email address. + """ + email: String! + id: ID! + + """ + Indicates whether this is the primary email of the associated user account. + """ + isPrimary: Boolean! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The user account to which the email belongs. + """ + userAccount: EnterpriseServerUserAccount! +} + +""" +The connection type for EnterpriseServerUserAccountEmail. +""" +type EnterpriseServerUserAccountEmailConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerUserAccountEmailEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerUserAccountEmail] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseServerUserAccountEmailEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerUserAccountEmail +} + +""" +Ordering options for Enterprise Server user account email connections. +""" +input EnterpriseServerUserAccountEmailOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order emails by. + """ + field: EnterpriseServerUserAccountEmailOrderField! +} + +""" +Properties by which Enterprise Server user account email connections can be ordered. +""" +enum EnterpriseServerUserAccountEmailOrderField { + """ + Order emails by email + """ + EMAIL +} + +""" +Ordering options for Enterprise Server user account connections. +""" +input EnterpriseServerUserAccountOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order user accounts by. + """ + field: EnterpriseServerUserAccountOrderField! +} + +""" +Properties by which Enterprise Server user account connections can be ordered. +""" +enum EnterpriseServerUserAccountOrderField { + """ + Order user accounts by login + """ + LOGIN + + """ + Order user accounts by creation time on the Enterprise Server installation + """ + REMOTE_CREATED_AT +} + +""" +A user accounts upload from an Enterprise Server installation. +""" +type EnterpriseServerUserAccountsUpload implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The enterprise to which this upload belongs. + """ + enterprise: Enterprise! + + """ + The Enterprise Server installation for which this upload was generated. + """ + enterpriseServerInstallation: EnterpriseServerInstallation! + id: ID! + + """ + The name of the file uploaded. + """ + name: String! + + """ + The synchronization state of the upload + """ + syncState: EnterpriseServerUserAccountsUploadSyncState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for EnterpriseServerUserAccountsUpload. +""" +type EnterpriseServerUserAccountsUploadConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerUserAccountsUploadEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerUserAccountsUpload] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseServerUserAccountsUploadEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerUserAccountsUpload +} + +""" +Ordering options for Enterprise Server user accounts upload connections. +""" +input EnterpriseServerUserAccountsUploadOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order user accounts uploads by. + """ + field: EnterpriseServerUserAccountsUploadOrderField! +} + +""" +Properties by which Enterprise Server user accounts upload connections can be ordered. +""" +enum EnterpriseServerUserAccountsUploadOrderField { + """ + Order user accounts uploads by creation time + """ + CREATED_AT +} + +""" +Synchronization state of the Enterprise Server user accounts upload +""" +enum EnterpriseServerUserAccountsUploadSyncState { + """ + The synchronization of the upload failed. + """ + FAILURE + + """ + The synchronization of the upload is pending. + """ + PENDING + + """ + The synchronization of the upload succeeded. + """ + SUCCESS +} + +""" +An account for a user who is an admin of an enterprise or a member of an enterprise through one or more organizations. +""" +type EnterpriseUserAccount implements Actor & Node { + """ + A URL pointing to the enterprise user account's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The enterprise in which this user account exists. + """ + enterprise: Enterprise! + + """ + A list of Enterprise Server installations this user is a member of. + """ + enterpriseInstallations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for installations returned from the connection. + """ + orderBy: EnterpriseServerInstallationOrder = {field: HOST_NAME, direction: ASC} + + """ + The search string to look for. + """ + query: String + + """ + The role of the user in the installation. + """ + role: EnterpriseUserAccountMembershipRole + ): EnterpriseServerInstallationMembershipConnection! + id: ID! + + """ + An identifier for the enterprise user account, a login or email address + """ + login: String! + + """ + The name of the enterprise user account + """ + name: String + + """ + A list of enterprise organizations this user is a member of. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations returned from the connection. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The search string to look for. + """ + query: String + + """ + The role of the user in the enterprise organization. + """ + role: EnterpriseUserAccountMembershipRole + ): EnterpriseOrganizationMembershipConnection! + + """ + The HTTP path for this user. + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this user. + """ + url: URI! + + """ + The user within the enterprise. + """ + user: User +} + +""" +The possible roles for enterprise membership. +""" +enum EnterpriseUserAccountMembershipRole { + """ + The user is a member of an organization in the enterprise. + """ + MEMBER + + """ + The user is an owner of an organization in the enterprise. + """ + OWNER + + """ + The user is not an owner of the enterprise, and not a member or owner of any + organizations in the enterprise; only for EMU-enabled enterprises. + """ + UNAFFILIATED +} + +""" +The possible GitHub Enterprise deployments where this user can exist. +""" +enum EnterpriseUserDeployment { + """ + The user is part of a GitHub Enterprise Cloud deployment. + """ + CLOUD + + """ + The user is part of a GitHub Enterprise Server deployment. + """ + SERVER +} + +""" +An environment. +""" +type Environment implements Node { + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The name of the environment + """ + name: String! + + """ + The protection rules defined for this environment + """ + protectionRules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentProtectionRuleConnection! +} + +""" +The connection type for Environment. +""" +type EnvironmentConnection { + """ + A list of edges. + """ + edges: [EnvironmentEdge] + + """ + A list of nodes. + """ + nodes: [Environment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnvironmentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Environment +} + +""" +Properties by which environments connections can be ordered +""" +enum EnvironmentOrderField { + """ + Order environments by name. + """ + NAME +} + +""" +Ordering options for environments +""" +input Environments { + """ + The direction in which to order environments by the specified field. + """ + direction: OrderDirection! + + """ + The field to order environments by. + """ + field: EnvironmentOrderField! +} + +""" +An external identity provisioned by SAML SSO or SCIM. If SAML is configured on +the organization, the external identity is visible to (1) organization owners, +(2) organization owners' personal access tokens (classic) with read:org or +admin:org scope, (3) GitHub App with an installation token with read or write +access to members. If SAML is configured on the enterprise, the external +identity is visible to (1) enterprise owners, (2) enterprise owners' personal +access tokens (classic) with read:enterprise or admin:enterprise scope. +""" +type ExternalIdentity implements Node { + """ + The GUID for this identity + """ + guid: String! + id: ID! + + """ + Organization invitation for this SCIM-provisioned external identity + """ + organizationInvitation: OrganizationInvitation + + """ + SAML Identity attributes + """ + samlIdentity: ExternalIdentitySamlAttributes + + """ + SCIM Identity attributes + """ + scimIdentity: ExternalIdentityScimAttributes + + """ + User linked to this external identity. Will be NULL if this identity has not been claimed by an organization member. + """ + user: User +} + +""" +An attribute for the External Identity attributes collection +""" +type ExternalIdentityAttribute { + """ + The attribute metadata as JSON + """ + metadata: String + + """ + The attribute name + """ + name: String! + + """ + The attribute value + """ + value: String! +} + +""" +The connection type for ExternalIdentity. +""" +type ExternalIdentityConnection { + """ + A list of edges. + """ + edges: [ExternalIdentityEdge] + + """ + A list of nodes. + """ + nodes: [ExternalIdentity] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ExternalIdentityEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ExternalIdentity +} + +""" +SAML attributes for the External Identity +""" +type ExternalIdentitySamlAttributes { + """ + SAML Identity attributes + """ + attributes: [ExternalIdentityAttribute!]! + + """ + The emails associated with the SAML identity + """ + emails: [UserEmailMetadata!] + + """ + Family name of the SAML identity + """ + familyName: String + + """ + Given name of the SAML identity + """ + givenName: String + + """ + The groups linked to this identity in IDP + """ + groups: [String!] + + """ + The NameID of the SAML identity + """ + nameId: String + + """ + The userName of the SAML identity + """ + username: String +} + +""" +SCIM attributes for the External Identity +""" +type ExternalIdentityScimAttributes { + """ + The emails associated with the SCIM identity + """ + emails: [UserEmailMetadata!] + + """ + Family name of the SCIM identity + """ + familyName: String + + """ + Given name of the SCIM identity + """ + givenName: String + + """ + The groups linked to this identity in IDP + """ + groups: [String!] + + """ + The userName of the SCIM identity + """ + username: String +} + +""" +A command to add a file at the given path with the given contents as part of a +commit. Any existing file at that that path will be replaced. +""" +input FileAddition { + """ + The base64 encoded contents of the file + """ + contents: Base64String! + + """ + The path in the repository where the file will be located + """ + path: String! +} + +""" +A description of a set of changes to a file tree to be made as part of +a git commit, modeled as zero or more file `additions` and zero or more +file `deletions`. + +Both fields are optional; omitting both will produce a commit with no +file changes. + +`deletions` and `additions` describe changes to files identified +by their path in the git tree using unix-style path separators, i.e. +`/`. The root of a git tree is an empty string, so paths are not +slash-prefixed. + +`path` values must be unique across all `additions` and `deletions` +provided. Any duplication will result in a validation error. + +### Encoding + +File contents must be provided in full for each `FileAddition`. + +The `contents` of a `FileAddition` must be encoded using RFC 4648 +compliant base64, i.e. correct padding is required and no characters +outside the standard alphabet may be used. Invalid base64 +encoding will be rejected with a validation error. + +The encoded contents may be binary. + +For text files, no assumptions are made about the character encoding of +the file contents (after base64 decoding). No charset transcoding or +line-ending normalization will be performed; it is the client's +responsibility to manage the character encoding of files they provide. +However, for maximum compatibility we recommend using UTF-8 encoding +and ensuring that all files in a repository use a consistent +line-ending convention (`\n` or `\r\n`), and that all files end +with a newline. + +### Modeling file changes + +Each of the the five types of conceptual changes that can be made in a +git commit can be described using the `FileChanges` type as follows: + +1. New file addition: create file `hello world\n` at path `docs/README.txt`: + + { + "additions" [ + { + "path": "docs/README.txt", + "contents": base64encode("hello world\n") + } + ] + } + +2. Existing file modification: change existing `docs/README.txt` to have new + content `new content here\n`: + + { + "additions" [ + { + "path": "docs/README.txt", + "contents": base64encode("new content here\n") + } + ] + } + +3. Existing file deletion: remove existing file `docs/README.txt`. + Note that the path is required to exist -- specifying a + path that does not exist on the given branch will abort the + commit and return an error. + + { + "deletions" [ + { + "path": "docs/README.txt" + } + ] + } + + +4. File rename with no changes: rename `docs/README.txt` with + previous content `hello world\n` to the same content at + `newdocs/README.txt`: + + { + "deletions" [ + { + "path": "docs/README.txt", + } + ], + "additions" [ + { + "path": "newdocs/README.txt", + "contents": base64encode("hello world\n") + } + ] + } + + +5. File rename with changes: rename `docs/README.txt` with + previous content `hello world\n` to a file at path + `newdocs/README.txt` with content `new contents\n`: + + { + "deletions" [ + { + "path": "docs/README.txt", + } + ], + "additions" [ + { + "path": "newdocs/README.txt", + "contents": base64encode("new contents\n") + } + ] + } +""" +input FileChanges { + """ + File to add or change. + """ + additions: [FileAddition!] = [] + + """ + Files to delete. + """ + deletions: [FileDeletion!] = [] +} + +""" +A command to delete the file at the given path as part of a commit. +""" +input FileDeletion { + """ + The path to delete + """ + path: String! +} + +""" +The possible viewed states of a file . +""" +enum FileViewedState { + """ + The file has new changes since last viewed. + """ + DISMISSED + + """ + The file has not been marked as viewed. + """ + UNVIEWED + + """ + The file has been marked as viewed. + """ + VIEWED +} + +""" +Autogenerated input type of FollowOrganization +""" +input FollowOrganizationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the organization to follow. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of FollowOrganization +""" +type FollowOrganizationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organization that was followed. + """ + organization: Organization +} + +""" +Autogenerated input type of FollowUser +""" +input FollowUserInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the user to follow. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of FollowUser +""" +type FollowUserPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The user that was followed. + """ + user: User +} + +""" +The connection type for User. +""" +type FollowerConnection { + """ + A list of edges. + """ + edges: [UserEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The connection type for User. +""" +type FollowingConnection { + """ + A list of edges. + """ + edges: [UserEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A funding platform link for a repository. +""" +type FundingLink { + """ + The funding platform this link is for. + """ + platform: FundingPlatform! + + """ + The configured URL for this funding link. + """ + url: URI! +} + +""" +The possible funding platforms for repository funding links. +""" +enum FundingPlatform { + """ + Community Bridge funding platform. + """ + COMMUNITY_BRIDGE + + """ + Custom funding platform. + """ + CUSTOM + + """ + GitHub funding platform. + """ + GITHUB + + """ + IssueHunt funding platform. + """ + ISSUEHUNT + + """ + Ko-fi funding platform. + """ + KO_FI + + """ + LFX Crowdfunding funding platform. + """ + LFX_CROWDFUNDING + + """ + Liberapay funding platform. + """ + LIBERAPAY + + """ + Open Collective funding platform. + """ + OPEN_COLLECTIVE + + """ + Otechie funding platform. + """ + OTECHIE + + """ + Patreon funding platform. + """ + PATREON + + """ + Tidelift funding platform. + """ + TIDELIFT +} + +""" +A generic hovercard context with a message and icon +""" +type GenericHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! +} + +""" +A Gist. +""" +type Gist implements Node & Starrable & UniformResourceLocatable { + """ + A list of comments associated with the gist + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): GistCommentConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The gist description. + """ + description: String + + """ + The files in this gist. + """ + files( + """ + The maximum number of files to return. + """ + limit: Int = 10 + + """ + The oid of the files to return + """ + oid: GitObjectID + ): [GistFile] + + """ + A list of forks associated with the gist + """ + forks( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for gists returned from the connection + """ + orderBy: GistOrder + ): GistConnection! + id: ID! + + """ + Identifies if the gist is a fork. + """ + isFork: Boolean! + + """ + Whether the gist is public or not. + """ + isPublic: Boolean! + + """ + The gist name. + """ + name: String! + + """ + The gist owner. + """ + owner: RepositoryOwner + + """ + Identifies when the gist was last pushed to. + """ + pushedAt: DateTime + + """ + The HTML path to this resource. + """ + resourcePath: URI! + + """ + Returns a count of how many stargazers there are on this object + """ + stargazerCount: Int! + + """ + A list of users who have starred this starrable. + """ + stargazers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + ): StargazerConnection! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this Gist. + """ + url: URI! + + """ + Returns a boolean indicating whether the viewing user has starred this starrable. + """ + viewerHasStarred: Boolean! +} + +""" +Represents a comment on an Gist. +""" +type GistComment implements Comment & Deletable & Minimizable & Node & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the gist. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Identifies the comment body. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + The associated gist. + """ + gist: Gist! + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for GistComment. +""" +type GistCommentConnection { + """ + A list of edges. + """ + edges: [GistCommentEdge] + + """ + A list of nodes. + """ + nodes: [GistComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type GistCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: GistComment +} + +""" +The connection type for Gist. +""" +type GistConnection { + """ + A list of edges. + """ + edges: [GistEdge] + + """ + A list of nodes. + """ + nodes: [Gist] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type GistEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Gist +} + +""" +A file in a gist. +""" +type GistFile { + """ + The file name encoded to remove characters that are invalid in URL paths. + """ + encodedName: String + + """ + The gist file encoding. + """ + encoding: String + + """ + The file extension from the file name. + """ + extension: String + + """ + Indicates if this file is an image. + """ + isImage: Boolean! + + """ + Whether the file's contents were truncated. + """ + isTruncated: Boolean! + + """ + The programming language this file is written in. + """ + language: Language + + """ + The gist file name. + """ + name: String + + """ + The gist file size in bytes. + """ + size: Int + + """ + UTF8 text data or null if the file is binary + """ + text( + """ + Optionally truncate the returned file to this length. + """ + truncate: Int + ): String +} + +""" +Ordering options for gist connections +""" +input GistOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repositories by. + """ + field: GistOrderField! +} + +""" +Properties by which gist connections can be ordered. +""" +enum GistOrderField { + """ + Order gists by creation time + """ + CREATED_AT + + """ + Order gists by push time + """ + PUSHED_AT + + """ + Order gists by update time + """ + UPDATED_AT +} + +""" +The privacy of a Gist +""" +enum GistPrivacy { + """ + Gists that are public and secret + """ + ALL + + """ + Public + """ + PUBLIC + + """ + Secret + """ + SECRET +} + +""" +Represents an actor in a Git commit (ie. an author or committer). +""" +type GitActor { + """ + A URL pointing to the author's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The timestamp of the Git action (authoring or committing). + """ + date: GitTimestamp + + """ + The email in the Git commit. + """ + email: String + + """ + The name in the Git commit. + """ + name: String + + """ + The GitHub user corresponding to the email field. Null if no such user exists. + """ + user: User +} + +""" +The connection type for GitActor. +""" +type GitActorConnection { + """ + A list of edges. + """ + edges: [GitActorEdge] + + """ + A list of nodes. + """ + nodes: [GitActor] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type GitActorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: GitActor +} + +""" +Represents information about the GitHub instance. +""" +type GitHubMetadata { + """ + Returns a String that's a SHA of `github-services` + """ + gitHubServicesSha: GitObjectID! + + """ + IP addresses that users connect to for git operations + """ + gitIpAddresses: [String!] + + """ + IP addresses that GitHub Enterprise Importer uses for outbound connections + """ + githubEnterpriseImporterIpAddresses: [String!] + + """ + IP addresses that service hooks are sent from + """ + hookIpAddresses: [String!] + + """ + IP addresses that the importer connects from + """ + importerIpAddresses: [String!] + + """ + Whether or not users are verified + """ + isPasswordAuthenticationVerifiable: Boolean! + + """ + IP addresses for GitHub Pages' A records + """ + pagesIpAddresses: [String!] +} + +""" +Represents a Git object. +""" +interface GitObject { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + id: ID! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The Repository the Git object belongs to + """ + repository: Repository! +} + +""" +A Git object ID. +""" +scalar GitObjectID + +""" +A fully qualified reference name (e.g. `refs/heads/master`). +""" +scalar GitRefname @preview(toggledBy: "update-refs-preview") + +""" +Git SSH string +""" +scalar GitSSHRemote + +""" +Information about a signature (GPG or S/MIME) on a Commit or Tag. +""" +interface GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +The state of a Git signature. +""" +enum GitSignatureState { + """ + The signing certificate or its chain could not be verified + """ + BAD_CERT + + """ + Invalid email used for signing + """ + BAD_EMAIL + + """ + Signing key expired + """ + EXPIRED_KEY + + """ + Internal error - the GPG verification service misbehaved + """ + GPGVERIFY_ERROR + + """ + Internal error - the GPG verification service is unavailable at the moment + """ + GPGVERIFY_UNAVAILABLE + + """ + Invalid signature + """ + INVALID + + """ + Malformed signature + """ + MALFORMED_SIG + + """ + The usage flags for the key that signed this don't allow signing + """ + NOT_SIGNING_KEY + + """ + Email used for signing not known to GitHub + """ + NO_USER + + """ + Valid signature, though certificate revocation check failed + """ + OCSP_ERROR + + """ + Valid signature, pending certificate revocation checking + """ + OCSP_PENDING + + """ + One or more certificates in chain has been revoked + """ + OCSP_REVOKED + + """ + Key used for signing not known to GitHub + """ + UNKNOWN_KEY + + """ + Unknown signature type + """ + UNKNOWN_SIG_TYPE + + """ + Unsigned + """ + UNSIGNED + + """ + Email used for signing unverified on GitHub + """ + UNVERIFIED_EMAIL + + """ + Valid signature and verified by GitHub + """ + VALID +} + +""" +An ISO-8601 encoded date string. Unlike the DateTime type, GitTimestamp is not converted in UTC. +""" +scalar GitTimestamp + +""" +Represents a GPG signature on a Commit or Tag. +""" +type GpgSignature implements GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Hex-encoded ID of the key that signed this object. + """ + keyId: String + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +Autogenerated input type of GrantEnterpriseOrganizationsMigratorRole +""" +input GrantEnterpriseOrganizationsMigratorRoleInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise to which all organizations managed by it will be granted the migrator role. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of the user to grant the migrator role + """ + login: String! +} + +""" +Autogenerated return type of GrantEnterpriseOrganizationsMigratorRole +""" +type GrantEnterpriseOrganizationsMigratorRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organizations that had the migrator role applied to for the given user. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationConnection +} + +""" +Autogenerated input type of GrantMigratorRole +""" +input GrantMigratorRoleInput { + """ + The user login or Team slug to grant the migrator role. + """ + actor: String! + + """ + Specifies the type of the actor, can be either USER or TEAM. + """ + actorType: ActorType! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the organization that the user/team belongs to. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of GrantMigratorRole +""" +type GrantMigratorRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Did the operation succeed? + """ + success: Boolean +} + +""" +A string containing HTML code. +""" +scalar HTML + +""" +Represents a 'head_ref_deleted' event on a given pull request. +""" +type HeadRefDeletedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the Ref associated with the `head_ref_deleted` event. + """ + headRef: Ref + + """ + Identifies the name of the Ref associated with the `head_ref_deleted` event. + """ + headRefName: String! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Represents a 'head_ref_force_pushed' event on a given pull request. +""" +type HeadRefForcePushedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the after commit SHA for the 'head_ref_force_pushed' event. + """ + afterCommit: Commit + + """ + Identifies the before commit SHA for the 'head_ref_force_pushed' event. + """ + beforeCommit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the fully qualified ref name for the 'head_ref_force_pushed' event. + """ + ref: Ref +} + +""" +Represents a 'head_ref_restored' event on a given pull request. +""" +type HeadRefRestoredEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Detail needed to display a hovercard for a user +""" +type Hovercard { + """ + Each of the contexts for this hovercard + """ + contexts: [HovercardContext!]! +} + +""" +An individual line of a hovercard +""" +interface HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! +} + +""" +The possible states in which authentication can be configured with an identity provider. +""" +enum IdentityProviderConfigurationState { + """ + Authentication with an identity provider is configured but not enforced. + """ + CONFIGURED + + """ + Authentication with an identity provider is configured and enforced. + """ + ENFORCED + + """ + Authentication with an identity provider is not configured. + """ + UNCONFIGURED +} + +""" +Autogenerated input type of ImportProject +""" +input ImportProjectInput { + """ + The description of Project. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A list of columns containing issues and pull requests. + """ + columnImports: [ProjectColumnImport!]! + + """ + The name of Project. + """ + name: String! + + """ + The name of the Organization or User to create the Project under. + """ + ownerName: String! + + """ + Whether the Project is public or not. + """ + public: Boolean = false +} + +""" +Autogenerated return type of ImportProject +""" +type ImportProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new Project! + """ + project: Project +} + +""" +Autogenerated input type of InviteEnterpriseAdmin +""" +input InviteEnterpriseAdminInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The email of the person to invite as an administrator. + """ + email: String + + """ + The ID of the enterprise to which you want to invite an administrator. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a user to invite as an administrator. + """ + invitee: String + + """ + The role of the administrator. + """ + role: EnterpriseAdministratorRole +} + +""" +Autogenerated return type of InviteEnterpriseAdmin +""" +type InviteEnterpriseAdminPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The created enterprise administrator invitation. + """ + invitation: EnterpriseAdministratorInvitation +} + +""" +The possible values for the IP allow list enabled setting. +""" +enum IpAllowListEnabledSettingValue { + """ + The setting is disabled for the owner. + """ + DISABLED + + """ + The setting is enabled for the owner. + """ + ENABLED +} + +""" +An IP address or range of addresses that is allowed to access an owner's resources. +""" +type IpAllowListEntry implements Node { + """ + A single IP address or range of IP addresses in CIDR notation. + """ + allowListValue: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Whether the entry is currently active. + """ + isActive: Boolean! + + """ + The name of the IP allow list entry. + """ + name: String + + """ + The owner of the IP allow list entry. + """ + owner: IpAllowListOwner! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for IpAllowListEntry. +""" +type IpAllowListEntryConnection { + """ + A list of edges. + """ + edges: [IpAllowListEntryEdge] + + """ + A list of nodes. + """ + nodes: [IpAllowListEntry] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type IpAllowListEntryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IpAllowListEntry +} + +""" +Ordering options for IP allow list entry connections. +""" +input IpAllowListEntryOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order IP allow list entries by. + """ + field: IpAllowListEntryOrderField! +} + +""" +Properties by which IP allow list entry connections can be ordered. +""" +enum IpAllowListEntryOrderField { + """ + Order IP allow list entries by the allow list value. + """ + ALLOW_LIST_VALUE + + """ + Order IP allow list entries by creation time. + """ + CREATED_AT +} + +""" +The possible values for the IP allow list configuration for installed GitHub Apps setting. +""" +enum IpAllowListForInstalledAppsEnabledSettingValue { + """ + The setting is disabled for the owner. + """ + DISABLED + + """ + The setting is enabled for the owner. + """ + ENABLED +} + +""" +Types that can own an IP allow list. +""" +union IpAllowListOwner = App | Enterprise | Organization + +""" +An Issue is a place to discuss ideas, enhancements, tasks, and bugs for a project. +""" +type Issue implements Assignable & Closable & Comment & Deletable & Labelable & Lockable & Node & ProjectV2Owner & Reactable & RepositoryNode & Subscribable & SubscribableThread & UniformResourceLocatable & Updatable & UpdatableComment { + """ + Reason that the conversation was locked. + """ + activeLockReason: LockReason + + """ + A list of Users assigned to this object. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Identifies the body of the issue. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The http path for this issue body + """ + bodyResourcePath: URI! + + """ + Identifies the body of the issue rendered to text. + """ + bodyText: String! + + """ + The http URL for this issue body + """ + bodyUrl: URI! + + """ + Indicates if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + A list of comments associated with the Issue. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issue comments returned from the connection. + """ + orderBy: IssueCommentOrder + ): IssueCommentConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + + """ + The hovercard information for this issue + """ + hovercard( + """ + Whether or not to include notification contexts + """ + includeNotificationContexts: Boolean = true + ): Hovercard! + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Indicates whether or not this issue is currently pinned to the repository issues list + """ + isPinned: Boolean + + """ + Is this issue read by the viewer + """ + isReadByViewer: Boolean + + """ + A list of labels associated with the object. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Branches linked to this issue. + """ + linkedBranches( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): LinkedBranchConnection! + + """ + `true` if the object is locked + """ + locked: Boolean! + + """ + Identifies the milestone associated with the issue. + """ + milestone: Milestone + + """ + Identifies the issue number. + """ + number: Int! + + """ + A list of Users that are participating in the Issue conversation. + """ + participants( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + List of project cards associated with this issue. + """ + projectCards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + A list of archived states to filter the cards by + """ + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectCardConnection! + + """ + List of project items associated with this issue. + """ + projectItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Include archived items. + """ + includeArchived: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2ItemConnection! + + """ + Find a project by number. + """ + projectV2( + """ + The project number. + """ + number: Int! + ): ProjectV2 + + """ + A list of projects under the owner. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + A project to search for under the the owner. + """ + query: String + ): ProjectV2Connection! + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path for this issue + """ + resourcePath: URI! + + """ + Identifies the state of the issue. + """ + state: IssueState! + + """ + Identifies the reason for the issue state. + """ + stateReason: IssueStateReason + + """ + A list of events, comments, commits, etc. associated with the issue. + """ + timeline( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows filtering timeline events by a `since` timestamp. + """ + since: DateTime + ): IssueTimelineConnection! + @deprecated(reason: "`timeline` will be removed Use Issue.timelineItems instead. Removal on 2020-10-01 UTC.") + + """ + A list of events, comments, commits, etc. associated with the issue. + """ + timelineItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter timeline items by type. + """ + itemTypes: [IssueTimelineItemsItemType!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter timeline items by a `since` timestamp. + """ + since: DateTime + + """ + Skips the first _n_ elements in the list. + """ + skip: Int + ): IssueTimelineItemsConnection! + + """ + Identifies the issue title. + """ + title: String! + + """ + Identifies the issue title rendered to HTML. + """ + titleHTML: String! + + """ + A list of issues that track this issue + """ + trackedInIssues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): IssueConnection! + + """ + A list of issues tracked inside the current issue + """ + trackedIssues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): IssueConnection! + + """ + The number of tracked issues for this issue + """ + trackedIssuesCount( + """ + Limit the count to tracked issues with the specified states. + """ + states: [TrackedIssueStates] + ): Int! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this issue + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState + + """ + Identifies the viewer's thread subscription form action. + """ + viewerThreadSubscriptionFormAction: ThreadSubscriptionFormAction + + """ + Identifies the viewer's thread subscription status. + """ + viewerThreadSubscriptionStatus: ThreadSubscriptionState +} + +""" +The possible state reasons of a closed issue. +""" +enum IssueClosedStateReason { + """ + An issue that has been closed as completed + """ + COMPLETED + + """ + An issue that has been closed as not planned + """ + NOT_PLANNED +} + +""" +Represents a comment on an Issue. +""" +type IssueComment implements Comment & Deletable & Minimizable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + Identifies the issue associated with the comment. + """ + issue: Issue! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + Returns the pull request associated with the comment, if this comment was made on a + pull request. + """ + pullRequest: PullRequest + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path for this issue comment + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this issue comment + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for IssueComment. +""" +type IssueCommentConnection { + """ + A list of edges. + """ + edges: [IssueCommentEdge] + + """ + A list of nodes. + """ + nodes: [IssueComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type IssueCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IssueComment +} + +""" +Ways in which lists of issue comments can be ordered upon return. +""" +input IssueCommentOrder { + """ + The direction in which to order issue comments by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order issue comments by. + """ + field: IssueCommentOrderField! +} + +""" +Properties by which issue comment connections can be ordered. +""" +enum IssueCommentOrderField { + """ + Order issue comments by update time + """ + UPDATED_AT +} + +""" +The connection type for Issue. +""" +type IssueConnection { + """ + A list of edges. + """ + edges: [IssueEdge] + + """ + A list of nodes. + """ + nodes: [Issue] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +This aggregates issues opened by a user within one repository. +""" +type IssueContributionsByRepository { + """ + The issue contributions. + """ + contributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedIssueContributionConnection! + + """ + The repository in which the issues were opened. + """ + repository: Repository! +} + +""" +An edge in a connection. +""" +type IssueEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Issue +} + +""" +Ways in which to filter lists of issues. +""" +input IssueFilters { + """ + List issues assigned to given name. Pass in `null` for issues with no assigned + user, and `*` for issues assigned to any user. + """ + assignee: String + + """ + List issues created by given name. + """ + createdBy: String + + """ + List issues where the list of label names exist on the issue. + """ + labels: [String!] + + """ + List issues where the given name is mentioned in the issue. + """ + mentioned: String + + """ + List issues by given milestone argument. If an string representation of an + integer is passed, it should refer to a milestone by its database ID. Pass in + `null` for issues with no milestone, and `*` for issues that are assigned to any milestone. + """ + milestone: String + + """ + List issues by given milestone argument. If an string representation of an + integer is passed, it should refer to a milestone by its number field. Pass in + `null` for issues with no milestone, and `*` for issues that are assigned to any milestone. + """ + milestoneNumber: String + + """ + List issues that have been updated at or after the given date. + """ + since: DateTime + + """ + List issues filtered by the list of states given. + """ + states: [IssueState!] + + """ + List issues subscribed to by viewer. + """ + viewerSubscribed: Boolean = false +} + +""" +Used for return value of Repository.issueOrPullRequest. +""" +union IssueOrPullRequest = Issue | PullRequest + +""" +Ways in which lists of issues can be ordered upon return. +""" +input IssueOrder { + """ + The direction in which to order issues by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order issues by. + """ + field: IssueOrderField! +} + +""" +Properties by which issue connections can be ordered. +""" +enum IssueOrderField { + """ + Order issues by comment count + """ + COMMENTS + + """ + Order issues by creation time + """ + CREATED_AT + + """ + Order issues by update time + """ + UPDATED_AT +} + +""" +The possible states of an issue. +""" +enum IssueState { + """ + An issue that has been closed + """ + CLOSED + + """ + An issue that is still open + """ + OPEN +} + +""" +The possible state reasons of an issue. +""" +enum IssueStateReason { + """ + An issue that has been closed as completed + """ + COMPLETED + + """ + An issue that has been closed as not planned + """ + NOT_PLANNED + + """ + An issue that has been reopened + """ + REOPENED +} + +""" +A repository issue template. +""" +type IssueTemplate { + """ + The template purpose. + """ + about: String + + """ + The suggested assignees. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The suggested issue body. + """ + body: String + + """ + The template filename. + """ + filename: String! + + """ + The suggested issue labels + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection + + """ + The template name. + """ + name: String! + + """ + The suggested issue title. + """ + title: String +} + +""" +The connection type for IssueTimelineItem. +""" +type IssueTimelineConnection { + """ + A list of edges. + """ + edges: [IssueTimelineItemEdge] + + """ + A list of nodes. + """ + nodes: [IssueTimelineItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An item in an issue timeline +""" +union IssueTimelineItem = + AssignedEvent + | ClosedEvent + | Commit + | CrossReferencedEvent + | DemilestonedEvent + | IssueComment + | LabeledEvent + | LockedEvent + | MilestonedEvent + | ReferencedEvent + | RenamedTitleEvent + | ReopenedEvent + | SubscribedEvent + | TransferredEvent + | UnassignedEvent + | UnlabeledEvent + | UnlockedEvent + | UnsubscribedEvent + | UserBlockedEvent + +""" +An edge in a connection. +""" +type IssueTimelineItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IssueTimelineItem +} + +""" +An item in an issue timeline +""" +union IssueTimelineItems = + AddedToProjectEvent + | AssignedEvent + | ClosedEvent + | CommentDeletedEvent + | ConnectedEvent + | ConvertedNoteToIssueEvent + | ConvertedToDiscussionEvent + | CrossReferencedEvent + | DemilestonedEvent + | DisconnectedEvent + | IssueComment + | LabeledEvent + | LockedEvent + | MarkedAsDuplicateEvent + | MentionedEvent + | MilestonedEvent + | MovedColumnsInProjectEvent + | PinnedEvent + | ReferencedEvent + | RemovedFromProjectEvent + | RenamedTitleEvent + | ReopenedEvent + | SubscribedEvent + | TransferredEvent + | UnassignedEvent + | UnlabeledEvent + | UnlockedEvent + | UnmarkedAsDuplicateEvent + | UnpinnedEvent + | UnsubscribedEvent + | UserBlockedEvent + +""" +The connection type for IssueTimelineItems. +""" +type IssueTimelineItemsConnection { + """ + A list of edges. + """ + edges: [IssueTimelineItemsEdge] + + """ + Identifies the count of items after applying `before` and `after` filters. + """ + filteredCount: Int! + + """ + A list of nodes. + """ + nodes: [IssueTimelineItems] + + """ + Identifies the count of items after applying `before`/`after` filters and `first`/`last`/`skip` slicing. + """ + pageCount: Int! + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Identifies the date and time when the timeline was last updated. + """ + updatedAt: DateTime! +} + +""" +An edge in a connection. +""" +type IssueTimelineItemsEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IssueTimelineItems +} + +""" +The possible item types found in a timeline. +""" +enum IssueTimelineItemsItemType { + """ + Represents a 'added_to_project' event on a given issue or pull request. + """ + ADDED_TO_PROJECT_EVENT + + """ + Represents an 'assigned' event on any assignable object. + """ + ASSIGNED_EVENT + + """ + Represents a 'closed' event on any `Closable`. + """ + CLOSED_EVENT + + """ + Represents a 'comment_deleted' event on a given issue or pull request. + """ + COMMENT_DELETED_EVENT + + """ + Represents a 'connected' event on a given issue or pull request. + """ + CONNECTED_EVENT + + """ + Represents a 'converted_note_to_issue' event on a given issue or pull request. + """ + CONVERTED_NOTE_TO_ISSUE_EVENT + + """ + Represents a 'converted_to_discussion' event on a given issue. + """ + CONVERTED_TO_DISCUSSION_EVENT + + """ + Represents a mention made by one issue or pull request to another. + """ + CROSS_REFERENCED_EVENT + + """ + Represents a 'demilestoned' event on a given issue or pull request. + """ + DEMILESTONED_EVENT + + """ + Represents a 'disconnected' event on a given issue or pull request. + """ + DISCONNECTED_EVENT + + """ + Represents a comment on an Issue. + """ + ISSUE_COMMENT + + """ + Represents a 'labeled' event on a given issue or pull request. + """ + LABELED_EVENT + + """ + Represents a 'locked' event on a given issue or pull request. + """ + LOCKED_EVENT + + """ + Represents a 'marked_as_duplicate' event on a given issue or pull request. + """ + MARKED_AS_DUPLICATE_EVENT + + """ + Represents a 'mentioned' event on a given issue or pull request. + """ + MENTIONED_EVENT + + """ + Represents a 'milestoned' event on a given issue or pull request. + """ + MILESTONED_EVENT + + """ + Represents a 'moved_columns_in_project' event on a given issue or pull request. + """ + MOVED_COLUMNS_IN_PROJECT_EVENT + + """ + Represents a 'pinned' event on a given issue or pull request. + """ + PINNED_EVENT + + """ + Represents a 'referenced' event on a given `ReferencedSubject`. + """ + REFERENCED_EVENT + + """ + Represents a 'removed_from_project' event on a given issue or pull request. + """ + REMOVED_FROM_PROJECT_EVENT + + """ + Represents a 'renamed' event on a given issue or pull request + """ + RENAMED_TITLE_EVENT + + """ + Represents a 'reopened' event on any `Closable`. + """ + REOPENED_EVENT + + """ + Represents a 'subscribed' event on a given `Subscribable`. + """ + SUBSCRIBED_EVENT + + """ + Represents a 'transferred' event on a given issue or pull request. + """ + TRANSFERRED_EVENT + + """ + Represents an 'unassigned' event on any assignable object. + """ + UNASSIGNED_EVENT + + """ + Represents an 'unlabeled' event on a given issue or pull request. + """ + UNLABELED_EVENT + + """ + Represents an 'unlocked' event on a given issue or pull request. + """ + UNLOCKED_EVENT + + """ + Represents an 'unmarked_as_duplicate' event on a given issue or pull request. + """ + UNMARKED_AS_DUPLICATE_EVENT + + """ + Represents an 'unpinned' event on a given issue or pull request. + """ + UNPINNED_EVENT + + """ + Represents an 'unsubscribed' event on a given `Subscribable`. + """ + UNSUBSCRIBED_EVENT + + """ + Represents a 'user_blocked' event on a given user. + """ + USER_BLOCKED_EVENT +} + +""" +Represents a user signing up for a GitHub account. +""" +type JoinedGitHubContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +A label for categorizing Issues, Pull Requests, Milestones, or Discussions with a given Repository. +""" +type Label implements Node { + """ + Identifies the label color. + """ + color: String! + + """ + Identifies the date and time when the label was created. + """ + createdAt: DateTime + + """ + A brief description of this label. + """ + description: String + id: ID! + + """ + Indicates whether or not this is a default label. + """ + isDefault: Boolean! + + """ + A list of issues associated with this label. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + Identifies the label name. + """ + name: String! + + """ + A list of pull requests associated with this label. + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + The repository associated with this label. + """ + repository: Repository! + + """ + The HTTP path for this label. + """ + resourcePath: URI! + + """ + Identifies the date and time when the label was last updated. + """ + updatedAt: DateTime + + """ + The HTTP URL for this label. + """ + url: URI! +} + +""" +The connection type for Label. +""" +type LabelConnection { + """ + A list of edges. + """ + edges: [LabelEdge] + + """ + A list of nodes. + """ + nodes: [Label] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type LabelEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Label +} + +""" +Ways in which lists of labels can be ordered upon return. +""" +input LabelOrder { + """ + The direction in which to order labels by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order labels by. + """ + field: LabelOrderField! +} + +""" +Properties by which label connections can be ordered. +""" +enum LabelOrderField { + """ + Order labels by creation time + """ + CREATED_AT + + """ + Order labels by name + """ + NAME +} + +""" +An object that can have labels assigned to it. +""" +interface Labelable { + """ + A list of labels associated with the object. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection +} + +""" +Represents a 'labeled' event on a given issue or pull request. +""" +type LabeledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the label associated with the 'labeled' event. + """ + label: Label! + + """ + Identifies the `Labelable` associated with the event. + """ + labelable: Labelable! +} + +""" +Represents a given language found in repositories. +""" +type Language implements Node { + """ + The color defined for the current language. + """ + color: String + id: ID! + + """ + The name of the current language. + """ + name: String! +} + +""" +A list of languages associated with the parent. +""" +type LanguageConnection { + """ + A list of edges. + """ + edges: [LanguageEdge] + + """ + A list of nodes. + """ + nodes: [Language] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + The total size in bytes of files written in that language. + """ + totalSize: Int! +} + +""" +Represents the language of a repository. +""" +type LanguageEdge { + cursor: String! + node: Language! + + """ + The number of bytes of code written in the language. + """ + size: Int! +} + +""" +Ordering options for language connections. +""" +input LanguageOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order languages by. + """ + field: LanguageOrderField! +} + +""" +Properties by which language connections can be ordered. +""" +enum LanguageOrderField { + """ + Order languages by the size of all files containing the language + """ + SIZE +} + +""" +A repository's open source license +""" +type License implements Node { + """ + The full text of the license + """ + body: String! + + """ + The conditions set by the license + """ + conditions: [LicenseRule]! + + """ + A human-readable description of the license + """ + description: String + + """ + Whether the license should be featured + """ + featured: Boolean! + + """ + Whether the license should be displayed in license pickers + """ + hidden: Boolean! + id: ID! + + """ + Instructions on how to implement the license + """ + implementation: String + + """ + The lowercased SPDX ID of the license + """ + key: String! + + """ + The limitations set by the license + """ + limitations: [LicenseRule]! + + """ + The license full name specified by + """ + name: String! + + """ + Customary short name if applicable (e.g, GPLv3) + """ + nickname: String + + """ + The permissions set by the license + """ + permissions: [LicenseRule]! + + """ + Whether the license is a pseudo-license placeholder (e.g., other, no-license) + """ + pseudoLicense: Boolean! + + """ + Short identifier specified by + """ + spdxId: String + + """ + URL to the license on + """ + url: URI +} + +""" +Describes a License's conditions, permissions, and limitations +""" +type LicenseRule { + """ + A description of the rule + """ + description: String! + + """ + The machine-readable rule key + """ + key: String! + + """ + The human-readable rule label + """ + label: String! +} + +""" +Autogenerated input type of LinkProjectV2ToRepository +""" +input LinkProjectV2ToRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the project to link to the repository. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The ID of the repository to link to the project. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of LinkProjectV2ToRepository +""" +type LinkProjectV2ToRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository the project is linked to. + """ + repository: Repository +} + +""" +Autogenerated input type of LinkProjectV2ToTeam +""" +input LinkProjectV2ToTeamInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the project to link to the team. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The ID of the team to link to the project. + """ + teamId: ID! @possibleTypes(concreteTypes: ["Team"]) +} + +""" +Autogenerated return type of LinkProjectV2ToTeam +""" +type LinkProjectV2ToTeamPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The team the project is linked to + """ + team: Team +} + +""" +Autogenerated input type of LinkRepositoryToProject +""" +input LinkRepositoryToProjectInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project to link to a Repository + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) + + """ + The ID of the Repository to link to a Project. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of LinkRepositoryToProject +""" +type LinkRepositoryToProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The linked Project. + """ + project: Project + + """ + The linked Repository. + """ + repository: Repository +} + +""" +A branch linked to an issue. +""" +type LinkedBranch implements Node { + id: ID! + + """ + The branch's ref. + """ + ref: Ref +} + +""" +The connection type for LinkedBranch. +""" +type LinkedBranchConnection { + """ + A list of edges. + """ + edges: [LinkedBranchEdge] + + """ + A list of nodes. + """ + nodes: [LinkedBranch] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type LinkedBranchEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: LinkedBranch +} + +""" +Autogenerated input type of LockLockable +""" +input LockLockableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A reason for why the item will be locked. + """ + lockReason: LockReason + + """ + ID of the item to be locked. + """ + lockableId: ID! @possibleTypes(concreteTypes: ["Discussion", "Issue", "PullRequest"], abstractType: "Lockable") +} + +""" +Autogenerated return type of LockLockable +""" +type LockLockablePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was locked. + """ + lockedRecord: Lockable +} + +""" +The possible reasons that an issue or pull request was locked. +""" +enum LockReason { + """ + The issue or pull request was locked because the conversation was off-topic. + """ + OFF_TOPIC + + """ + The issue or pull request was locked because the conversation was resolved. + """ + RESOLVED + + """ + The issue or pull request was locked because the conversation was spam. + """ + SPAM + + """ + The issue or pull request was locked because the conversation was too heated. + """ + TOO_HEATED +} + +""" +An object that can be locked. +""" +interface Lockable { + """ + Reason that the conversation was locked. + """ + activeLockReason: LockReason + + """ + `true` if the object is locked + """ + locked: Boolean! +} + +""" +Represents a 'locked' event on a given issue or pull request. +""" +type LockedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Reason that the conversation was locked (optional). + """ + lockReason: LockReason + + """ + Object that was locked. + """ + lockable: Lockable! +} + +""" +A placeholder user for attribution of imported data on GitHub. +""" +type Mannequin implements Actor & Node & UniformResourceLocatable { + """ + A URL pointing to the GitHub App's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The user that has claimed the data attributed to this mannequin. + """ + claimant: User + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The mannequin's email on the source instance. + """ + email: String + id: ID! + + """ + The username of the actor. + """ + login: String! + + """ + The HTML path to this resource. + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The URL to this resource. + """ + url: URI! +} + +""" +The connection type for Mannequin. +""" +type MannequinConnection { + """ + A list of edges. + """ + edges: [MannequinEdge] + + """ + A list of nodes. + """ + nodes: [Mannequin] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a mannequin. +""" +type MannequinEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Mannequin +} + +""" +Ordering options for mannequins. +""" +input MannequinOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order mannequins by. + """ + field: MannequinOrderField! +} + +""" +Properties by which mannequins can be ordered. +""" +enum MannequinOrderField { + """ + Order mannequins why when they were created. + """ + CREATED_AT + + """ + Order mannequins alphabetically by their source login. + """ + LOGIN +} + +""" +Autogenerated input type of MarkDiscussionCommentAsAnswer +""" +input MarkDiscussionCommentAsAnswerInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion comment to mark as an answer. + """ + id: ID! @possibleTypes(concreteTypes: ["DiscussionComment"]) +} + +""" +Autogenerated return type of MarkDiscussionCommentAsAnswer +""" +type MarkDiscussionCommentAsAnswerPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion that includes the chosen comment. + """ + discussion: Discussion +} + +""" +Autogenerated input type of MarkFileAsViewed +""" +input MarkFileAsViewedInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The path of the file to mark as viewed + """ + path: String! + + """ + The Node ID of the pull request. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of MarkFileAsViewed +""" +type MarkFileAsViewedPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of MarkProjectV2AsTemplate +""" +input MarkProjectV2AsTemplateInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project to mark as a template. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of MarkProjectV2AsTemplate +""" +type MarkProjectV2AsTemplatePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The project. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of MarkPullRequestReadyForReview +""" +input MarkPullRequestReadyForReviewInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to be marked as ready for review. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of MarkPullRequestReadyForReview +""" +type MarkPullRequestReadyForReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that is ready for review. + """ + pullRequest: PullRequest +} + +""" +Represents a 'marked_as_duplicate' event on a given issue or pull request. +""" +type MarkedAsDuplicateEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + The authoritative issue or pull request which has been duplicated by another. + """ + canonical: IssueOrPullRequest + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The issue or pull request which has been marked as a duplicate of another. + """ + duplicate: IssueOrPullRequest + id: ID! + + """ + Canonical and duplicate belong to different repositories. + """ + isCrossRepository: Boolean! +} + +""" +A public description of a Marketplace category. +""" +type MarketplaceCategory implements Node { + """ + The category's description. + """ + description: String + + """ + The technical description of how apps listed in this category work with GitHub. + """ + howItWorks: String + id: ID! + + """ + The category's name. + """ + name: String! + + """ + How many Marketplace listings have this as their primary category. + """ + primaryListingCount: Int! + + """ + The HTTP path for this Marketplace category. + """ + resourcePath: URI! + + """ + How many Marketplace listings have this as their secondary category. + """ + secondaryListingCount: Int! + + """ + The short name of the category used in its URL. + """ + slug: String! + + """ + The HTTP URL for this Marketplace category. + """ + url: URI! +} + +""" +A listing in the GitHub integration marketplace. +""" +type MarketplaceListing implements Node { + """ + The GitHub App this listing represents. + """ + app: App + + """ + URL to the listing owner's company site. + """ + companyUrl: URI + + """ + The HTTP path for configuring access to the listing's integration or OAuth app + """ + configurationResourcePath: URI! + + """ + The HTTP URL for configuring access to the listing's integration or OAuth app + """ + configurationUrl: URI! + + """ + URL to the listing's documentation. + """ + documentationUrl: URI + + """ + The listing's detailed description. + """ + extendedDescription: String + + """ + The listing's detailed description rendered to HTML. + """ + extendedDescriptionHTML: HTML! + + """ + The listing's introductory description. + """ + fullDescription: String! + + """ + The listing's introductory description rendered to HTML. + """ + fullDescriptionHTML: HTML! + + """ + Does this listing have any plans with a free trial? + """ + hasPublishedFreeTrialPlans: Boolean! + + """ + Does this listing have a terms of service link? + """ + hasTermsOfService: Boolean! + + """ + Whether the creator of the app is a verified org + """ + hasVerifiedOwner: Boolean! + + """ + A technical description of how this app works with GitHub. + """ + howItWorks: String + + """ + The listing's technical description rendered to HTML. + """ + howItWorksHTML: HTML! + id: ID! + + """ + URL to install the product to the viewer's account or organization. + """ + installationUrl: URI + + """ + Whether this listing's app has been installed for the current viewer + """ + installedForViewer: Boolean! + + """ + Whether this listing has been removed from the Marketplace. + """ + isArchived: Boolean! + + """ + Whether this listing is still an editable draft that has not been submitted + for review and is not publicly visible in the Marketplace. + """ + isDraft: Boolean! + + """ + Whether the product this listing represents is available as part of a paid plan. + """ + isPaid: Boolean! + + """ + Whether this listing has been approved for display in the Marketplace. + """ + isPublic: Boolean! + + """ + Whether this listing has been rejected by GitHub for display in the Marketplace. + """ + isRejected: Boolean! + + """ + Whether this listing has been approved for unverified display in the Marketplace. + """ + isUnverified: Boolean! + + """ + Whether this draft listing has been submitted for review for approval to be unverified in the Marketplace. + """ + isUnverifiedPending: Boolean! + + """ + Whether this draft listing has been submitted for review from GitHub for approval to be verified in the Marketplace. + """ + isVerificationPendingFromDraft: Boolean! + + """ + Whether this unverified listing has been submitted for review from GitHub for approval to be verified in the Marketplace. + """ + isVerificationPendingFromUnverified: Boolean! + + """ + Whether this listing has been approved for verified display in the Marketplace. + """ + isVerified: Boolean! + + """ + The hex color code, without the leading '#', for the logo background. + """ + logoBackgroundColor: String! + + """ + URL for the listing's logo image. + """ + logoUrl( + """ + The size in pixels of the resulting square image. + """ + size: Int = 400 + ): URI + + """ + The listing's full name. + """ + name: String! + + """ + The listing's very short description without a trailing period or ampersands. + """ + normalizedShortDescription: String! + + """ + URL to the listing's detailed pricing. + """ + pricingUrl: URI + + """ + The category that best describes the listing. + """ + primaryCategory: MarketplaceCategory! + + """ + URL to the listing's privacy policy, may return an empty string for listings that do not require a privacy policy URL. + """ + privacyPolicyUrl: URI! + + """ + The HTTP path for the Marketplace listing. + """ + resourcePath: URI! + + """ + The URLs for the listing's screenshots. + """ + screenshotUrls: [String]! + + """ + An alternate category that describes the listing. + """ + secondaryCategory: MarketplaceCategory + + """ + The listing's very short description. + """ + shortDescription: String! + + """ + The short name of the listing used in its URL. + """ + slug: String! + + """ + URL to the listing's status page. + """ + statusUrl: URI + + """ + An email address for support for this listing's app. + """ + supportEmail: String + + """ + Either a URL or an email address for support for this listing's app, may + return an empty string for listings that do not require a support URL. + """ + supportUrl: URI! + + """ + URL to the listing's terms of service. + """ + termsOfServiceUrl: URI + + """ + The HTTP URL for the Marketplace listing. + """ + url: URI! + + """ + Can the current viewer add plans for this Marketplace listing. + """ + viewerCanAddPlans: Boolean! + + """ + Can the current viewer approve this Marketplace listing. + """ + viewerCanApprove: Boolean! + + """ + Can the current viewer delist this Marketplace listing. + """ + viewerCanDelist: Boolean! + + """ + Can the current viewer edit this Marketplace listing. + """ + viewerCanEdit: Boolean! + + """ + Can the current viewer edit the primary and secondary category of this + Marketplace listing. + """ + viewerCanEditCategories: Boolean! + + """ + Can the current viewer edit the plans for this Marketplace listing. + """ + viewerCanEditPlans: Boolean! + + """ + Can the current viewer return this Marketplace listing to draft state + so it becomes editable again. + """ + viewerCanRedraft: Boolean! + + """ + Can the current viewer reject this Marketplace listing by returning it to + an editable draft state or rejecting it entirely. + """ + viewerCanReject: Boolean! + + """ + Can the current viewer request this listing be reviewed for display in + the Marketplace as verified. + """ + viewerCanRequestApproval: Boolean! + + """ + Indicates whether the current user has an active subscription to this Marketplace listing. + """ + viewerHasPurchased: Boolean! + + """ + Indicates if the current user has purchased a subscription to this Marketplace listing + for all of the organizations the user owns. + """ + viewerHasPurchasedForAllOrganizations: Boolean! + + """ + Does the current viewer role allow them to administer this Marketplace listing. + """ + viewerIsListingAdmin: Boolean! +} + +""" +Look up Marketplace Listings +""" +type MarketplaceListingConnection { + """ + A list of edges. + """ + edges: [MarketplaceListingEdge] + + """ + A list of nodes. + """ + nodes: [MarketplaceListing] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MarketplaceListingEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: MarketplaceListing +} + +""" +Entities that have members who can set status messages. +""" +interface MemberStatusable { + """ + Get the status messages members of this entity have set that are either public or visible only to the organization. + """ + memberStatuses( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for user statuses returned from the connection. + """ + orderBy: UserStatusOrder = {field: UPDATED_AT, direction: DESC} + ): UserStatusConnection! +} + +""" +Audit log entry for a members_can_delete_repos.clear event. +""" +type MembersCanDeleteReposClearAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a members_can_delete_repos.disable event. +""" +type MembersCanDeleteReposDisableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a members_can_delete_repos.enable event. +""" +type MembersCanDeleteReposEnableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Represents a 'mentioned' event on a given issue or pull request. +""" +type MentionedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! +} + +""" +Autogenerated input type of MergeBranch +""" +input MergeBranchInput { + """ + The email address to associate with this commit. + """ + authorEmail: String + + """ + The name of the base branch that the provided head will be merged into. + """ + base: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Message to use for the merge commit. If omitted, a default will be used. + """ + commitMessage: String + + """ + The head to merge into the base branch. This can be a branch name or a commit GitObjectID. + """ + head: String! + + """ + The Node ID of the Repository containing the base branch that will be modified. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of MergeBranch +""" +type MergeBranchPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The resulting merge Commit. + """ + mergeCommit: Commit +} + +""" +The possible default commit messages for merges. +""" +enum MergeCommitMessage { + """ + Default to a blank commit message. + """ + BLANK + + """ + Default to the pull request's body. + """ + PR_BODY + + """ + Default to the pull request's title. + """ + PR_TITLE +} + +""" +The possible default commit titles for merges. +""" +enum MergeCommitTitle { + """ + Default to the classic title for a merge message (e.g., Merge pull request #123 from branch-name). + """ + MERGE_MESSAGE + + """ + Default to the pull request's title. + """ + PR_TITLE +} + +""" +Autogenerated input type of MergePullRequest +""" +input MergePullRequestInput { + """ + The email address to associate with this merge. + """ + authorEmail: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Commit body to use for the merge commit; if omitted, a default message will be used + """ + commitBody: String + + """ + Commit headline to use for the merge commit; if omitted, a default message will be used. + """ + commitHeadline: String + + """ + OID that the pull request head ref must match to allow merge; if omitted, no check is performed. + """ + expectedHeadOid: GitObjectID + + """ + The merge method to use. If omitted, defaults to 'MERGE' + """ + mergeMethod: PullRequestMergeMethod = MERGE + + """ + ID of the pull request to be merged. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of MergePullRequest +""" +type MergePullRequestPayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that was merged. + """ + pullRequest: PullRequest +} + +""" +The queue of pull request entries to be merged into a protected branch in a repository. +""" +type MergeQueue implements Node { + """ + The configuration for this merge queue + """ + configuration: MergeQueueConfiguration + + """ + The entries in the queue + """ + entries( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): MergeQueueEntryConnection + id: ID! + + """ + The estimated time in seconds until a newly added entry would be merged + """ + nextEntryEstimatedTimeToMerge: Int + + """ + The repository this merge queue belongs to + """ + repository: Repository + + """ + The HTTP path for this merge queue + """ + resourcePath: URI! + + """ + The HTTP URL for this merge queue + """ + url: URI! +} + +""" +Configuration for a MergeQueue +""" +type MergeQueueConfiguration { + """ + The amount of time in minutes to wait for a check response before considering it a failure. + """ + checkResponseTimeout: Int + + """ + The maximum number of entries to build at once. + """ + maximumEntriesToBuild: Int + + """ + The maximum number of entries to merge at once. + """ + maximumEntriesToMerge: Int + + """ + The merge method to use for this queue. + """ + mergeMethod: PullRequestMergeMethod + + """ + The strategy to use when merging entries. + """ + mergingStrategy: MergeQueueMergingStrategy + + """ + The minimum number of entries required to merge at once. + """ + minimumEntriesToMerge: Int + + """ + The amount of time in minutes to wait before ignoring the minumum number of + entries in the queue requirement and merging a collection of entries + """ + minimumEntriesToMergeWaitTime: Int +} + +""" +Entries in a MergeQueue +""" +type MergeQueueEntry implements Node { + """ + The base commit for this entry + """ + baseCommit: Commit + + """ + The date and time this entry was added to the merge queue + """ + enqueuedAt: DateTime! + + """ + The actor that enqueued this entry + """ + enqueuer: Actor! + + """ + The estimated time in seconds until this entry will be merged + """ + estimatedTimeToMerge: Int + + """ + The head commit for this entry + """ + headCommit: Commit + id: ID! + + """ + Whether this pull request should jump the queue + """ + jump: Boolean! + + """ + The merge queue that this entry belongs to + """ + mergeQueue: MergeQueue + + """ + The position of this entry in the queue + """ + position: Int! + + """ + The pull request that will be added to a merge group + """ + pullRequest: PullRequest + + """ + Does this pull request need to be deployed on its own + """ + solo: Boolean! + + """ + The state of this entry in the queue + """ + state: MergeQueueEntryState! +} + +""" +The connection type for MergeQueueEntry. +""" +type MergeQueueEntryConnection { + """ + A list of edges. + """ + edges: [MergeQueueEntryEdge] + + """ + A list of nodes. + """ + nodes: [MergeQueueEntry] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MergeQueueEntryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: MergeQueueEntry +} + +""" +The possible states for a merge queue entry. +""" +enum MergeQueueEntryState { + """ + The entry is currently waiting for checks to pass. + """ + AWAITING_CHECKS + + """ + The entry is currently locked. + """ + LOCKED + + """ + The entry is currently mergeable. + """ + MERGEABLE + + """ + The entry is currently queued. + """ + QUEUED + + """ + The entry is currently unmergeable. + """ + UNMERGEABLE +} + +""" +The possible merging strategies for a merge queue. +""" +enum MergeQueueMergingStrategy { + """ + Entries only allowed to merge if they are passing. + """ + ALLGREEN + + """ + Failing Entires are allowed to merge if they are with a passing entry. + """ + HEADGREEN +} + +""" +Detailed status information about a pull request merge. +""" +enum MergeStateStatus { + """ + The head ref is out of date. + """ + BEHIND + + """ + The merge is blocked. + """ + BLOCKED + + """ + Mergeable and passing commit status. + """ + CLEAN + + """ + The merge commit cannot be cleanly created. + """ + DIRTY + + """ + The merge is blocked due to the pull request being a draft. + """ + DRAFT + @deprecated( + reason: "DRAFT state will be removed from this enum and `isDraft` should be used instead Use PullRequest.isDraft instead. Removal on 2021-01-01 UTC." + ) + + """ + Mergeable with passing commit status and pre-receive hooks. + """ + HAS_HOOKS + + """ + The state cannot currently be determined. + """ + UNKNOWN + + """ + Mergeable with non-passing commit status. + """ + UNSTABLE +} + +""" +Whether or not a PullRequest can be merged. +""" +enum MergeableState { + """ + The pull request cannot be merged due to merge conflicts. + """ + CONFLICTING + + """ + The pull request can be merged. + """ + MERGEABLE + + """ + The mergeability of the pull request is still being calculated. + """ + UNKNOWN +} + +""" +Represents a 'merged' event on a given pull request. +""" +type MergedEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the commit associated with the `merge` event. + """ + commit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the Ref associated with the `merge` event. + """ + mergeRef: Ref + + """ + Identifies the name of the Ref associated with the `merge` event. + """ + mergeRefName: String! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + The HTTP path for this merged event. + """ + resourcePath: URI! + + """ + The HTTP URL for this merged event. + """ + url: URI! +} + +""" +Represents a GitHub Enterprise Importer (GEI) migration. +""" +interface Migration { + """ + The migration flag to continue on error. + """ + continueOnError: Boolean! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: String + + """ + The reason the migration failed. + """ + failureReason: String + id: ID! + + """ + The URL for the migration log (expires 1 day after migration completes). + """ + migrationLogUrl: URI + + """ + The migration source. + """ + migrationSource: MigrationSource! + + """ + The target repository name. + """ + repositoryName: String! + + """ + The migration source URL, for example `https://github.com` or `https://monalisa.ghe.com`. + """ + sourceUrl: URI! + + """ + The migration state. + """ + state: MigrationState! + + """ + The number of warnings encountered for this migration. To review the warnings, + check the [Migration Log](https://docs.github.com/en/migrations/using-github-enterprise-importer/completing-your-migration-with-github-enterprise-importer/accessing-your-migration-logs-for-github-enterprise-importer). + """ + warningsCount: Int! +} + +""" +A GitHub Enterprise Importer (GEI) migration source. +""" +type MigrationSource implements Node { + id: ID! + + """ + The migration source name. + """ + name: String! + + """ + The migration source type. + """ + type: MigrationSourceType! + + """ + The migration source URL, for example `https://github.com` or `https://monalisa.ghe.com`. + """ + url: URI! +} + +""" +Represents the different GitHub Enterprise Importer (GEI) migration sources. +""" +enum MigrationSourceType { + """ + An Azure DevOps migration source. + """ + AZURE_DEVOPS + + """ + A Bitbucket Server migration source. + """ + BITBUCKET_SERVER + + """ + A GitHub Migration API source. + """ + GITHUB_ARCHIVE +} + +""" +The GitHub Enterprise Importer (GEI) migration state. +""" +enum MigrationState { + """ + The migration has failed. + """ + FAILED + + """ + The migration has invalid credentials. + """ + FAILED_VALIDATION + + """ + The migration is in progress. + """ + IN_PROGRESS + + """ + The migration has not started. + """ + NOT_STARTED + + """ + The migration needs to have its credentials validated. + """ + PENDING_VALIDATION + + """ + The migration has been queued. + """ + QUEUED + + """ + The migration has succeeded. + """ + SUCCEEDED +} + +""" +Represents a Milestone object on a given repository. +""" +type Milestone implements Closable & Node & UniformResourceLocatable { + """ + Indicates if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the actor who created the milestone. + """ + creator: Actor + + """ + Identifies the description of the milestone. + """ + description: String + + """ + Identifies the due date of the milestone. + """ + dueOn: DateTime + id: ID! + + """ + A list of issues associated with the milestone. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + Identifies the number of the milestone. + """ + number: Int! + + """ + Identifies the percentage complete for the milestone + """ + progressPercentage: Float! + + """ + A list of pull requests associated with the milestone. + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + The repository associated with this milestone. + """ + repository: Repository! + + """ + The HTTP path for this milestone + """ + resourcePath: URI! + + """ + Identifies the state of the milestone. + """ + state: MilestoneState! + + """ + Identifies the title of the milestone. + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this milestone + """ + url: URI! + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! +} + +""" +The connection type for Milestone. +""" +type MilestoneConnection { + """ + A list of edges. + """ + edges: [MilestoneEdge] + + """ + A list of nodes. + """ + nodes: [Milestone] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MilestoneEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Milestone +} + +""" +Types that can be inside a Milestone. +""" +union MilestoneItem = Issue | PullRequest + +""" +Ordering options for milestone connections. +""" +input MilestoneOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order milestones by. + """ + field: MilestoneOrderField! +} + +""" +Properties by which milestone connections can be ordered. +""" +enum MilestoneOrderField { + """ + Order milestones by when they were created. + """ + CREATED_AT + + """ + Order milestones by when they are due. + """ + DUE_DATE + + """ + Order milestones by their number. + """ + NUMBER + + """ + Order milestones by when they were last updated. + """ + UPDATED_AT +} + +""" +The possible states of a milestone. +""" +enum MilestoneState { + """ + A milestone that has been closed. + """ + CLOSED + + """ + A milestone that is still open. + """ + OPEN +} + +""" +Represents a 'milestoned' event on a given issue or pull request. +""" +type MilestonedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the milestone title associated with the 'milestoned' event. + """ + milestoneTitle: String! + + """ + Object referenced by event. + """ + subject: MilestoneItem! +} + +""" +Entities that can be minimized. +""" +interface Minimizable { + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! +} + +""" +Autogenerated input type of MinimizeComment +""" +input MinimizeCommentInput { + """ + The classification of comment + """ + classifier: ReportedContentClassifiers! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! + @possibleTypes( + concreteTypes: ["CommitComment", "DiscussionComment", "GistComment", "IssueComment", "PullRequestReviewComment"] + abstractType: "Minimizable" + ) +} + +""" +Autogenerated return type of MinimizeComment +""" +type MinimizeCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The comment that was minimized. + """ + minimizedComment: Minimizable +} + +""" +Autogenerated input type of MoveProjectCard +""" +input MoveProjectCardInput { + """ + Place the new card after the card with this id. Pass null to place it at the top. + """ + afterCardId: ID @possibleTypes(concreteTypes: ["ProjectCard"]) + + """ + The id of the card to move. + """ + cardId: ID! @possibleTypes(concreteTypes: ["ProjectCard"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the column to move it into. + """ + columnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of MoveProjectCard +""" +type MoveProjectCardPayload { + """ + The new edge of the moved card. + """ + cardEdge: ProjectCardEdge + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of MoveProjectColumn +""" +input MoveProjectColumnInput { + """ + Place the new column after the column with this id. Pass null to place it at the front. + """ + afterColumnId: ID @possibleTypes(concreteTypes: ["ProjectColumn"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the column to move. + """ + columnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of MoveProjectColumn +""" +type MoveProjectColumnPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new edge of the moved column. + """ + columnEdge: ProjectColumnEdge +} + +""" +Represents a 'moved_columns_in_project' event on a given issue or pull request. +""" +type MovedColumnsInProjectEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Column name the issue or pull request was moved from. + """ + previousProjectColumnName: String! @preview(toggledBy: "starfox-preview") + + """ + Project referenced by event. + """ + project: Project @preview(toggledBy: "starfox-preview") + + """ + Project card referenced by this project event. + """ + projectCard: ProjectCard @preview(toggledBy: "starfox-preview") + + """ + Column name the issue or pull request was moved to. + """ + projectColumnName: String! @preview(toggledBy: "starfox-preview") +} + +""" +The root query for implementing GraphQL mutations. +""" +type Mutation { + """ + Clear all of a customer's queued migrations + """ + abortQueuedMigrations( + """ + Parameters for AbortQueuedMigrations + """ + input: AbortQueuedMigrationsInput! + ): AbortQueuedMigrationsPayload + + """ + Accepts a pending invitation for a user to become an administrator of an enterprise. + """ + acceptEnterpriseAdministratorInvitation( + """ + Parameters for AcceptEnterpriseAdministratorInvitation + """ + input: AcceptEnterpriseAdministratorInvitationInput! + ): AcceptEnterpriseAdministratorInvitationPayload + + """ + Applies a suggested topic to the repository. + """ + acceptTopicSuggestion( + """ + Parameters for AcceptTopicSuggestion + """ + input: AcceptTopicSuggestionInput! + ): AcceptTopicSuggestionPayload + + """ + Adds assignees to an assignable object. + """ + addAssigneesToAssignable( + """ + Parameters for AddAssigneesToAssignable + """ + input: AddAssigneesToAssignableInput! + ): AddAssigneesToAssignablePayload + + """ + Adds a comment to an Issue or Pull Request. + """ + addComment( + """ + Parameters for AddComment + """ + input: AddCommentInput! + ): AddCommentPayload + + """ + Adds a comment to a Discussion, possibly as a reply to another comment. + """ + addDiscussionComment( + """ + Parameters for AddDiscussionComment + """ + input: AddDiscussionCommentInput! + ): AddDiscussionCommentPayload + + """ + Vote for an option in a discussion poll. + """ + addDiscussionPollVote( + """ + Parameters for AddDiscussionPollVote + """ + input: AddDiscussionPollVoteInput! + ): AddDiscussionPollVotePayload + + """ + Adds enterprise members to an organization within the enterprise. + """ + addEnterpriseOrganizationMember( + """ + Parameters for AddEnterpriseOrganizationMember + """ + input: AddEnterpriseOrganizationMemberInput! + ): AddEnterpriseOrganizationMemberPayload + + """ + Adds a support entitlement to an enterprise member. + """ + addEnterpriseSupportEntitlement( + """ + Parameters for AddEnterpriseSupportEntitlement + """ + input: AddEnterpriseSupportEntitlementInput! + ): AddEnterpriseSupportEntitlementPayload + + """ + Adds labels to a labelable object. + """ + addLabelsToLabelable( + """ + Parameters for AddLabelsToLabelable + """ + input: AddLabelsToLabelableInput! + ): AddLabelsToLabelablePayload + + """ + Adds a card to a ProjectColumn. Either `contentId` or `note` must be provided but **not** both. + """ + addProjectCard( + """ + Parameters for AddProjectCard + """ + input: AddProjectCardInput! + ): AddProjectCardPayload + + """ + Adds a column to a Project. + """ + addProjectColumn( + """ + Parameters for AddProjectColumn + """ + input: AddProjectColumnInput! + ): AddProjectColumnPayload + + """ + Creates a new draft issue and add it to a Project. + """ + addProjectV2DraftIssue( + """ + Parameters for AddProjectV2DraftIssue + """ + input: AddProjectV2DraftIssueInput! + ): AddProjectV2DraftIssuePayload + + """ + Links an existing content instance to a Project. + """ + addProjectV2ItemById( + """ + Parameters for AddProjectV2ItemById + """ + input: AddProjectV2ItemByIdInput! + ): AddProjectV2ItemByIdPayload + + """ + Adds a review to a Pull Request. + """ + addPullRequestReview( + """ + Parameters for AddPullRequestReview + """ + input: AddPullRequestReviewInput! + ): AddPullRequestReviewPayload + + """ + Adds a comment to a review. + """ + addPullRequestReviewComment( + """ + Parameters for AddPullRequestReviewComment + """ + input: AddPullRequestReviewCommentInput! + ): AddPullRequestReviewCommentPayload + + """ + Adds a new thread to a pending Pull Request Review. + """ + addPullRequestReviewThread( + """ + Parameters for AddPullRequestReviewThread + """ + input: AddPullRequestReviewThreadInput! + ): AddPullRequestReviewThreadPayload + + """ + Adds a reply to an existing Pull Request Review Thread. + """ + addPullRequestReviewThreadReply( + """ + Parameters for AddPullRequestReviewThreadReply + """ + input: AddPullRequestReviewThreadReplyInput! + ): AddPullRequestReviewThreadReplyPayload + + """ + Adds a reaction to a subject. + """ + addReaction( + """ + Parameters for AddReaction + """ + input: AddReactionInput! + ): AddReactionPayload + + """ + Adds a star to a Starrable. + """ + addStar( + """ + Parameters for AddStar + """ + input: AddStarInput! + ): AddStarPayload + + """ + Add an upvote to a discussion or discussion comment. + """ + addUpvote( + """ + Parameters for AddUpvote + """ + input: AddUpvoteInput! + ): AddUpvotePayload + + """ + Adds a verifiable domain to an owning account. + """ + addVerifiableDomain( + """ + Parameters for AddVerifiableDomain + """ + input: AddVerifiableDomainInput! + ): AddVerifiableDomainPayload + + """ + Approve all pending deployments under one or more environments + """ + approveDeployments( + """ + Parameters for ApproveDeployments + """ + input: ApproveDeploymentsInput! + ): ApproveDeploymentsPayload + + """ + Approve a verifiable domain for notification delivery. + """ + approveVerifiableDomain( + """ + Parameters for ApproveVerifiableDomain + """ + input: ApproveVerifiableDomainInput! + ): ApproveVerifiableDomainPayload + + """ + Archives a ProjectV2Item + """ + archiveProjectV2Item( + """ + Parameters for ArchiveProjectV2Item + """ + input: ArchiveProjectV2ItemInput! + ): ArchiveProjectV2ItemPayload + + """ + Marks a repository as archived. + """ + archiveRepository( + """ + Parameters for ArchiveRepository + """ + input: ArchiveRepositoryInput! + ): ArchiveRepositoryPayload + + """ + Cancels a pending invitation for an administrator to join an enterprise. + """ + cancelEnterpriseAdminInvitation( + """ + Parameters for CancelEnterpriseAdminInvitation + """ + input: CancelEnterpriseAdminInvitationInput! + ): CancelEnterpriseAdminInvitationPayload + + """ + Cancel an active sponsorship. + """ + cancelSponsorship( + """ + Parameters for CancelSponsorship + """ + input: CancelSponsorshipInput! + ): CancelSponsorshipPayload + + """ + Update your status on GitHub. + """ + changeUserStatus( + """ + Parameters for ChangeUserStatus + """ + input: ChangeUserStatusInput! + ): ChangeUserStatusPayload + + """ + Clears all labels from a labelable object. + """ + clearLabelsFromLabelable( + """ + Parameters for ClearLabelsFromLabelable + """ + input: ClearLabelsFromLabelableInput! + ): ClearLabelsFromLabelablePayload + + """ + This mutation clears the value of a field for an item in a Project. Currently + only text, number, date, assignees, labels, single-select, iteration and + milestone fields are supported. + """ + clearProjectV2ItemFieldValue( + """ + Parameters for ClearProjectV2ItemFieldValue + """ + input: ClearProjectV2ItemFieldValueInput! + ): ClearProjectV2ItemFieldValuePayload + + """ + Creates a new project by cloning configuration from an existing project. + """ + cloneProject( + """ + Parameters for CloneProject + """ + input: CloneProjectInput! + ): CloneProjectPayload + + """ + Create a new repository with the same files and directory structure as a template repository. + """ + cloneTemplateRepository( + """ + Parameters for CloneTemplateRepository + """ + input: CloneTemplateRepositoryInput! + ): CloneTemplateRepositoryPayload + + """ + Close a discussion. + """ + closeDiscussion( + """ + Parameters for CloseDiscussion + """ + input: CloseDiscussionInput! + ): CloseDiscussionPayload + + """ + Close an issue. + """ + closeIssue( + """ + Parameters for CloseIssue + """ + input: CloseIssueInput! + ): CloseIssuePayload + + """ + Close a pull request. + """ + closePullRequest( + """ + Parameters for ClosePullRequest + """ + input: ClosePullRequestInput! + ): ClosePullRequestPayload + + """ + Convert a project note card to one associated with a newly created issue. + """ + convertProjectCardNoteToIssue( + """ + Parameters for ConvertProjectCardNoteToIssue + """ + input: ConvertProjectCardNoteToIssueInput! + ): ConvertProjectCardNoteToIssuePayload + + """ + Converts a pull request to draft + """ + convertPullRequestToDraft( + """ + Parameters for ConvertPullRequestToDraft + """ + input: ConvertPullRequestToDraftInput! + ): ConvertPullRequestToDraftPayload + + """ + Copy a project. + """ + copyProjectV2( + """ + Parameters for CopyProjectV2 + """ + input: CopyProjectV2Input! + ): CopyProjectV2Payload + + """ + Invites a user to claim reattributable data + """ + createAttributionInvitation( + """ + Parameters for CreateAttributionInvitation + """ + input: CreateAttributionInvitationInput! + ): CreateAttributionInvitationPayload + + """ + Create a new branch protection rule + """ + createBranchProtectionRule( + """ + Parameters for CreateBranchProtectionRule + """ + input: CreateBranchProtectionRuleInput! + ): CreateBranchProtectionRulePayload + + """ + Create a check run. + """ + createCheckRun( + """ + Parameters for CreateCheckRun + """ + input: CreateCheckRunInput! + ): CreateCheckRunPayload + + """ + Create a check suite + """ + createCheckSuite( + """ + Parameters for CreateCheckSuite + """ + input: CreateCheckSuiteInput! + ): CreateCheckSuitePayload + + """ + Appends a commit to the given branch as the authenticated user. + + This mutation creates a commit whose parent is the HEAD of the provided + branch and also updates that branch to point to the new commit. + It can be thought of as similar to `git commit`. + + ### Locating a Branch + + Commits are appended to a `branch` of type `Ref`. + This must refer to a git branch (i.e. the fully qualified path must + begin with `refs/heads/`, although including this prefix is optional. + + Callers may specify the `branch` to commit to either by its global node + ID or by passing both of `repositoryNameWithOwner` and `refName`. For + more details see the documentation for `CommittableBranch`. + + ### Describing Changes + + `fileChanges` are specified as a `FilesChanges` object describing + `FileAdditions` and `FileDeletions`. + + Please see the documentation for `FileChanges` for more information on + how to use this argument to describe any set of file changes. + + ### Authorship + + Similar to the web commit interface, this mutation does not support + specifying the author or committer of the commit and will not add + support for this in the future. + + A commit created by a successful execution of this mutation will be + authored by the owner of the credential which authenticates the API + request. The committer will be identical to that of commits authored + using the web interface. + + If you need full control over author and committer information, please + use the Git Database REST API instead. + + ### Commit Signing + + Commits made using this mutation are automatically signed by GitHub if + supported and will be marked as verified in the user interface. + """ + createCommitOnBranch( + """ + Parameters for CreateCommitOnBranch + """ + input: CreateCommitOnBranchInput! + ): CreateCommitOnBranchPayload + + """ + Creates a new deployment event. + """ + createDeployment( + """ + Parameters for CreateDeployment + """ + input: CreateDeploymentInput! + ): CreateDeploymentPayload @preview(toggledBy: "flash-preview") + + """ + Create a deployment status. + """ + createDeploymentStatus( + """ + Parameters for CreateDeploymentStatus + """ + input: CreateDeploymentStatusInput! + ): CreateDeploymentStatusPayload @preview(toggledBy: "flash-preview") + + """ + Create a discussion. + """ + createDiscussion( + """ + Parameters for CreateDiscussion + """ + input: CreateDiscussionInput! + ): CreateDiscussionPayload + + """ + Creates an organization as part of an enterprise account. + """ + createEnterpriseOrganization( + """ + Parameters for CreateEnterpriseOrganization + """ + input: CreateEnterpriseOrganizationInput! + ): CreateEnterpriseOrganizationPayload + + """ + Creates an environment or simply returns it if already exists. + """ + createEnvironment( + """ + Parameters for CreateEnvironment + """ + input: CreateEnvironmentInput! + ): CreateEnvironmentPayload + + """ + Creates a new IP allow list entry. + """ + createIpAllowListEntry( + """ + Parameters for CreateIpAllowListEntry + """ + input: CreateIpAllowListEntryInput! + ): CreateIpAllowListEntryPayload + + """ + Creates a new issue. + """ + createIssue( + """ + Parameters for CreateIssue + """ + input: CreateIssueInput! + ): CreateIssuePayload + + """ + Creates a new label. + """ + createLabel( + """ + Parameters for CreateLabel + """ + input: CreateLabelInput! + ): CreateLabelPayload @preview(toggledBy: "bane-preview") + + """ + Create a branch linked to an issue. + """ + createLinkedBranch( + """ + Parameters for CreateLinkedBranch + """ + input: CreateLinkedBranchInput! + ): CreateLinkedBranchPayload + + """ + Creates a GitHub Enterprise Importer (GEI) migration source. + """ + createMigrationSource( + """ + Parameters for CreateMigrationSource + """ + input: CreateMigrationSourceInput! + ): CreateMigrationSourcePayload + + """ + Creates a new project. + """ + createProject( + """ + Parameters for CreateProject + """ + input: CreateProjectInput! + ): CreateProjectPayload + + """ + Creates a new project. + """ + createProjectV2( + """ + Parameters for CreateProjectV2 + """ + input: CreateProjectV2Input! + ): CreateProjectV2Payload + + """ + Create a new project field. + """ + createProjectV2Field( + """ + Parameters for CreateProjectV2Field + """ + input: CreateProjectV2FieldInput! + ): CreateProjectV2FieldPayload + + """ + Create a new pull request + """ + createPullRequest( + """ + Parameters for CreatePullRequest + """ + input: CreatePullRequestInput! + ): CreatePullRequestPayload + + """ + Create a new Git Ref. + """ + createRef( + """ + Parameters for CreateRef + """ + input: CreateRefInput! + ): CreateRefPayload + + """ + Create a new repository. + """ + createRepository( + """ + Parameters for CreateRepository + """ + input: CreateRepositoryInput! + ): CreateRepositoryPayload + + """ + Create a repository ruleset + """ + createRepositoryRuleset( + """ + Parameters for CreateRepositoryRuleset + """ + input: CreateRepositoryRulesetInput! + ): CreateRepositoryRulesetPayload + + """ + Create a GitHub Sponsors profile to allow others to sponsor you or your organization. + """ + createSponsorsListing( + """ + Parameters for CreateSponsorsListing + """ + input: CreateSponsorsListingInput! + ): CreateSponsorsListingPayload + + """ + Create a new payment tier for your GitHub Sponsors profile. + """ + createSponsorsTier( + """ + Parameters for CreateSponsorsTier + """ + input: CreateSponsorsTierInput! + ): CreateSponsorsTierPayload + + """ + Start a new sponsorship of a maintainer in GitHub Sponsors, or reactivate a past sponsorship. + """ + createSponsorship( + """ + Parameters for CreateSponsorship + """ + input: CreateSponsorshipInput! + ): CreateSponsorshipPayload + + """ + Make many one-time sponsorships for different sponsorable users or + organizations at once. Can only sponsor those who have a public GitHub + Sponsors profile. + """ + createSponsorships( + """ + Parameters for CreateSponsorships + """ + input: CreateSponsorshipsInput! + ): CreateSponsorshipsPayload + + """ + Creates a new team discussion. + """ + createTeamDiscussion( + """ + Parameters for CreateTeamDiscussion + """ + input: CreateTeamDiscussionInput! + ): CreateTeamDiscussionPayload + + """ + Creates a new team discussion comment. + """ + createTeamDiscussionComment( + """ + Parameters for CreateTeamDiscussionComment + """ + input: CreateTeamDiscussionCommentInput! + ): CreateTeamDiscussionCommentPayload + + """ + Rejects a suggested topic for the repository. + """ + declineTopicSuggestion( + """ + Parameters for DeclineTopicSuggestion + """ + input: DeclineTopicSuggestionInput! + ): DeclineTopicSuggestionPayload + + """ + Delete a branch protection rule + """ + deleteBranchProtectionRule( + """ + Parameters for DeleteBranchProtectionRule + """ + input: DeleteBranchProtectionRuleInput! + ): DeleteBranchProtectionRulePayload + + """ + Deletes a deployment. + """ + deleteDeployment( + """ + Parameters for DeleteDeployment + """ + input: DeleteDeploymentInput! + ): DeleteDeploymentPayload + + """ + Delete a discussion and all of its replies. + """ + deleteDiscussion( + """ + Parameters for DeleteDiscussion + """ + input: DeleteDiscussionInput! + ): DeleteDiscussionPayload + + """ + Delete a discussion comment. If it has replies, wipe it instead. + """ + deleteDiscussionComment( + """ + Parameters for DeleteDiscussionComment + """ + input: DeleteDiscussionCommentInput! + ): DeleteDiscussionCommentPayload + + """ + Deletes an environment + """ + deleteEnvironment( + """ + Parameters for DeleteEnvironment + """ + input: DeleteEnvironmentInput! + ): DeleteEnvironmentPayload + + """ + Deletes an IP allow list entry. + """ + deleteIpAllowListEntry( + """ + Parameters for DeleteIpAllowListEntry + """ + input: DeleteIpAllowListEntryInput! + ): DeleteIpAllowListEntryPayload + + """ + Deletes an Issue object. + """ + deleteIssue( + """ + Parameters for DeleteIssue + """ + input: DeleteIssueInput! + ): DeleteIssuePayload + + """ + Deletes an IssueComment object. + """ + deleteIssueComment( + """ + Parameters for DeleteIssueComment + """ + input: DeleteIssueCommentInput! + ): DeleteIssueCommentPayload + + """ + Deletes a label. + """ + deleteLabel( + """ + Parameters for DeleteLabel + """ + input: DeleteLabelInput! + ): DeleteLabelPayload @preview(toggledBy: "bane-preview") + + """ + Unlink a branch from an issue. + """ + deleteLinkedBranch( + """ + Parameters for DeleteLinkedBranch + """ + input: DeleteLinkedBranchInput! + ): DeleteLinkedBranchPayload + + """ + Delete a package version. + """ + deletePackageVersion( + """ + Parameters for DeletePackageVersion + """ + input: DeletePackageVersionInput! + ): DeletePackageVersionPayload @preview(toggledBy: "package-deletes-preview") + + """ + Deletes a project. + """ + deleteProject( + """ + Parameters for DeleteProject + """ + input: DeleteProjectInput! + ): DeleteProjectPayload + + """ + Deletes a project card. + """ + deleteProjectCard( + """ + Parameters for DeleteProjectCard + """ + input: DeleteProjectCardInput! + ): DeleteProjectCardPayload + + """ + Deletes a project column. + """ + deleteProjectColumn( + """ + Parameters for DeleteProjectColumn + """ + input: DeleteProjectColumnInput! + ): DeleteProjectColumnPayload + + """ + Delete a project. + """ + deleteProjectV2( + """ + Parameters for DeleteProjectV2 + """ + input: DeleteProjectV2Input! + ): DeleteProjectV2Payload + + """ + Delete a project field. + """ + deleteProjectV2Field( + """ + Parameters for DeleteProjectV2Field + """ + input: DeleteProjectV2FieldInput! + ): DeleteProjectV2FieldPayload + + """ + Deletes an item from a Project. + """ + deleteProjectV2Item( + """ + Parameters for DeleteProjectV2Item + """ + input: DeleteProjectV2ItemInput! + ): DeleteProjectV2ItemPayload + + """ + Deletes a project workflow. + """ + deleteProjectV2Workflow( + """ + Parameters for DeleteProjectV2Workflow + """ + input: DeleteProjectV2WorkflowInput! + ): DeleteProjectV2WorkflowPayload + + """ + Deletes a pull request review. + """ + deletePullRequestReview( + """ + Parameters for DeletePullRequestReview + """ + input: DeletePullRequestReviewInput! + ): DeletePullRequestReviewPayload + + """ + Deletes a pull request review comment. + """ + deletePullRequestReviewComment( + """ + Parameters for DeletePullRequestReviewComment + """ + input: DeletePullRequestReviewCommentInput! + ): DeletePullRequestReviewCommentPayload + + """ + Delete a Git Ref. + """ + deleteRef( + """ + Parameters for DeleteRef + """ + input: DeleteRefInput! + ): DeleteRefPayload + + """ + Delete a repository ruleset + """ + deleteRepositoryRuleset( + """ + Parameters for DeleteRepositoryRuleset + """ + input: DeleteRepositoryRulesetInput! + ): DeleteRepositoryRulesetPayload + + """ + Deletes a team discussion. + """ + deleteTeamDiscussion( + """ + Parameters for DeleteTeamDiscussion + """ + input: DeleteTeamDiscussionInput! + ): DeleteTeamDiscussionPayload + + """ + Deletes a team discussion comment. + """ + deleteTeamDiscussionComment( + """ + Parameters for DeleteTeamDiscussionComment + """ + input: DeleteTeamDiscussionCommentInput! + ): DeleteTeamDiscussionCommentPayload + + """ + Deletes a verifiable domain. + """ + deleteVerifiableDomain( + """ + Parameters for DeleteVerifiableDomain + """ + input: DeleteVerifiableDomainInput! + ): DeleteVerifiableDomainPayload + + """ + Remove a pull request from the merge queue. + """ + dequeuePullRequest( + """ + Parameters for DequeuePullRequest + """ + input: DequeuePullRequestInput! + ): DequeuePullRequestPayload + + """ + Disable auto merge on the given pull request + """ + disablePullRequestAutoMerge( + """ + Parameters for DisablePullRequestAutoMerge + """ + input: DisablePullRequestAutoMergeInput! + ): DisablePullRequestAutoMergePayload + + """ + Dismisses an approved or rejected pull request review. + """ + dismissPullRequestReview( + """ + Parameters for DismissPullRequestReview + """ + input: DismissPullRequestReviewInput! + ): DismissPullRequestReviewPayload + + """ + Dismisses the Dependabot alert. + """ + dismissRepositoryVulnerabilityAlert( + """ + Parameters for DismissRepositoryVulnerabilityAlert + """ + input: DismissRepositoryVulnerabilityAlertInput! + ): DismissRepositoryVulnerabilityAlertPayload + + """ + Enable the default auto-merge on a pull request. + """ + enablePullRequestAutoMerge( + """ + Parameters for EnablePullRequestAutoMerge + """ + input: EnablePullRequestAutoMergeInput! + ): EnablePullRequestAutoMergePayload + + """ + Add a pull request to the merge queue. + """ + enqueuePullRequest( + """ + Parameters for EnqueuePullRequest + """ + input: EnqueuePullRequestInput! + ): EnqueuePullRequestPayload + + """ + Follow an organization. + """ + followOrganization( + """ + Parameters for FollowOrganization + """ + input: FollowOrganizationInput! + ): FollowOrganizationPayload + + """ + Follow a user. + """ + followUser( + """ + Parameters for FollowUser + """ + input: FollowUserInput! + ): FollowUserPayload + + """ + Grant the migrator role to a user for all organizations under an enterprise account. + """ + grantEnterpriseOrganizationsMigratorRole( + """ + Parameters for GrantEnterpriseOrganizationsMigratorRole + """ + input: GrantEnterpriseOrganizationsMigratorRoleInput! + ): GrantEnterpriseOrganizationsMigratorRolePayload + + """ + Grant the migrator role to a user or a team. + """ + grantMigratorRole( + """ + Parameters for GrantMigratorRole + """ + input: GrantMigratorRoleInput! + ): GrantMigratorRolePayload + + """ + Creates a new project by importing columns and a list of issues/PRs. + """ + importProject( + """ + Parameters for ImportProject + """ + input: ImportProjectInput! + ): ImportProjectPayload @preview(toggledBy: "slothette-preview") + + """ + Invite someone to become an administrator of the enterprise. + """ + inviteEnterpriseAdmin( + """ + Parameters for InviteEnterpriseAdmin + """ + input: InviteEnterpriseAdminInput! + ): InviteEnterpriseAdminPayload + + """ + Links a project to a repository. + """ + linkProjectV2ToRepository( + """ + Parameters for LinkProjectV2ToRepository + """ + input: LinkProjectV2ToRepositoryInput! + ): LinkProjectV2ToRepositoryPayload + + """ + Links a project to a team. + """ + linkProjectV2ToTeam( + """ + Parameters for LinkProjectV2ToTeam + """ + input: LinkProjectV2ToTeamInput! + ): LinkProjectV2ToTeamPayload + + """ + Creates a repository link for a project. + """ + linkRepositoryToProject( + """ + Parameters for LinkRepositoryToProject + """ + input: LinkRepositoryToProjectInput! + ): LinkRepositoryToProjectPayload + + """ + Lock a lockable object + """ + lockLockable( + """ + Parameters for LockLockable + """ + input: LockLockableInput! + ): LockLockablePayload + + """ + Mark a discussion comment as the chosen answer for discussions in an answerable category. + """ + markDiscussionCommentAsAnswer( + """ + Parameters for MarkDiscussionCommentAsAnswer + """ + input: MarkDiscussionCommentAsAnswerInput! + ): MarkDiscussionCommentAsAnswerPayload + + """ + Mark a pull request file as viewed + """ + markFileAsViewed( + """ + Parameters for MarkFileAsViewed + """ + input: MarkFileAsViewedInput! + ): MarkFileAsViewedPayload + + """ + Mark a project as a template. Note that only projects which are owned by an Organization can be marked as a template. + """ + markProjectV2AsTemplate( + """ + Parameters for MarkProjectV2AsTemplate + """ + input: MarkProjectV2AsTemplateInput! + ): MarkProjectV2AsTemplatePayload + + """ + Marks a pull request ready for review. + """ + markPullRequestReadyForReview( + """ + Parameters for MarkPullRequestReadyForReview + """ + input: MarkPullRequestReadyForReviewInput! + ): MarkPullRequestReadyForReviewPayload + + """ + Merge a head into a branch. + """ + mergeBranch( + """ + Parameters for MergeBranch + """ + input: MergeBranchInput! + ): MergeBranchPayload + + """ + Merge a pull request. + """ + mergePullRequest( + """ + Parameters for MergePullRequest + """ + input: MergePullRequestInput! + ): MergePullRequestPayload + + """ + Minimizes a comment on an Issue, Commit, Pull Request, or Gist + """ + minimizeComment( + """ + Parameters for MinimizeComment + """ + input: MinimizeCommentInput! + ): MinimizeCommentPayload + + """ + Moves a project card to another place. + """ + moveProjectCard( + """ + Parameters for MoveProjectCard + """ + input: MoveProjectCardInput! + ): MoveProjectCardPayload + + """ + Moves a project column to another place. + """ + moveProjectColumn( + """ + Parameters for MoveProjectColumn + """ + input: MoveProjectColumnInput! + ): MoveProjectColumnPayload + + """ + Pin an issue to a repository + """ + pinIssue( + """ + Parameters for PinIssue + """ + input: PinIssueInput! + ): PinIssuePayload + + """ + Publish an existing sponsorship tier that is currently still a draft to a GitHub Sponsors profile. + """ + publishSponsorsTier( + """ + Parameters for PublishSponsorsTier + """ + input: PublishSponsorsTierInput! + ): PublishSponsorsTierPayload + + """ + Regenerates the identity provider recovery codes for an enterprise + """ + regenerateEnterpriseIdentityProviderRecoveryCodes( + """ + Parameters for RegenerateEnterpriseIdentityProviderRecoveryCodes + """ + input: RegenerateEnterpriseIdentityProviderRecoveryCodesInput! + ): RegenerateEnterpriseIdentityProviderRecoveryCodesPayload + + """ + Regenerates a verifiable domain's verification token. + """ + regenerateVerifiableDomainToken( + """ + Parameters for RegenerateVerifiableDomainToken + """ + input: RegenerateVerifiableDomainTokenInput! + ): RegenerateVerifiableDomainTokenPayload + + """ + Reject all pending deployments under one or more environments + """ + rejectDeployments( + """ + Parameters for RejectDeployments + """ + input: RejectDeploymentsInput! + ): RejectDeploymentsPayload + + """ + Removes assignees from an assignable object. + """ + removeAssigneesFromAssignable( + """ + Parameters for RemoveAssigneesFromAssignable + """ + input: RemoveAssigneesFromAssignableInput! + ): RemoveAssigneesFromAssignablePayload + + """ + Removes an administrator from the enterprise. + """ + removeEnterpriseAdmin( + """ + Parameters for RemoveEnterpriseAdmin + """ + input: RemoveEnterpriseAdminInput! + ): RemoveEnterpriseAdminPayload + + """ + Removes the identity provider from an enterprise + """ + removeEnterpriseIdentityProvider( + """ + Parameters for RemoveEnterpriseIdentityProvider + """ + input: RemoveEnterpriseIdentityProviderInput! + ): RemoveEnterpriseIdentityProviderPayload + + """ + Removes a user from all organizations within the enterprise + """ + removeEnterpriseMember( + """ + Parameters for RemoveEnterpriseMember + """ + input: RemoveEnterpriseMemberInput! + ): RemoveEnterpriseMemberPayload + + """ + Removes an organization from the enterprise + """ + removeEnterpriseOrganization( + """ + Parameters for RemoveEnterpriseOrganization + """ + input: RemoveEnterpriseOrganizationInput! + ): RemoveEnterpriseOrganizationPayload + + """ + Removes a support entitlement from an enterprise member. + """ + removeEnterpriseSupportEntitlement( + """ + Parameters for RemoveEnterpriseSupportEntitlement + """ + input: RemoveEnterpriseSupportEntitlementInput! + ): RemoveEnterpriseSupportEntitlementPayload + + """ + Removes labels from a Labelable object. + """ + removeLabelsFromLabelable( + """ + Parameters for RemoveLabelsFromLabelable + """ + input: RemoveLabelsFromLabelableInput! + ): RemoveLabelsFromLabelablePayload + + """ + Removes outside collaborator from all repositories in an organization. + """ + removeOutsideCollaborator( + """ + Parameters for RemoveOutsideCollaborator + """ + input: RemoveOutsideCollaboratorInput! + ): RemoveOutsideCollaboratorPayload + + """ + Removes a reaction from a subject. + """ + removeReaction( + """ + Parameters for RemoveReaction + """ + input: RemoveReactionInput! + ): RemoveReactionPayload + + """ + Removes a star from a Starrable. + """ + removeStar( + """ + Parameters for RemoveStar + """ + input: RemoveStarInput! + ): RemoveStarPayload + + """ + Remove an upvote to a discussion or discussion comment. + """ + removeUpvote( + """ + Parameters for RemoveUpvote + """ + input: RemoveUpvoteInput! + ): RemoveUpvotePayload + + """ + Reopen a discussion. + """ + reopenDiscussion( + """ + Parameters for ReopenDiscussion + """ + input: ReopenDiscussionInput! + ): ReopenDiscussionPayload + + """ + Reopen a issue. + """ + reopenIssue( + """ + Parameters for ReopenIssue + """ + input: ReopenIssueInput! + ): ReopenIssuePayload + + """ + Reopen a pull request. + """ + reopenPullRequest( + """ + Parameters for ReopenPullRequest + """ + input: ReopenPullRequestInput! + ): ReopenPullRequestPayload + + """ + Set review requests on a pull request. + """ + requestReviews( + """ + Parameters for RequestReviews + """ + input: RequestReviewsInput! + ): RequestReviewsPayload + + """ + Rerequests an existing check suite. + """ + rerequestCheckSuite( + """ + Parameters for RerequestCheckSuite + """ + input: RerequestCheckSuiteInput! + ): RerequestCheckSuitePayload + + """ + Marks a review thread as resolved. + """ + resolveReviewThread( + """ + Parameters for ResolveReviewThread + """ + input: ResolveReviewThreadInput! + ): ResolveReviewThreadPayload + + """ + Retire a published payment tier from your GitHub Sponsors profile so it cannot be used to start new sponsorships. + """ + retireSponsorsTier( + """ + Parameters for RetireSponsorsTier + """ + input: RetireSponsorsTierInput! + ): RetireSponsorsTierPayload + + """ + Create a pull request that reverts the changes from a merged pull request. + """ + revertPullRequest( + """ + Parameters for RevertPullRequest + """ + input: RevertPullRequestInput! + ): RevertPullRequestPayload + + """ + Revoke the migrator role to a user for all organizations under an enterprise account. + """ + revokeEnterpriseOrganizationsMigratorRole( + """ + Parameters for RevokeEnterpriseOrganizationsMigratorRole + """ + input: RevokeEnterpriseOrganizationsMigratorRoleInput! + ): RevokeEnterpriseOrganizationsMigratorRolePayload + + """ + Revoke the migrator role from a user or a team. + """ + revokeMigratorRole( + """ + Parameters for RevokeMigratorRole + """ + input: RevokeMigratorRoleInput! + ): RevokeMigratorRolePayload + + """ + Creates or updates the identity provider for an enterprise. + """ + setEnterpriseIdentityProvider( + """ + Parameters for SetEnterpriseIdentityProvider + """ + input: SetEnterpriseIdentityProviderInput! + ): SetEnterpriseIdentityProviderPayload + + """ + Set an organization level interaction limit for an organization's public repositories. + """ + setOrganizationInteractionLimit( + """ + Parameters for SetOrganizationInteractionLimit + """ + input: SetOrganizationInteractionLimitInput! + ): SetOrganizationInteractionLimitPayload + + """ + Sets an interaction limit setting for a repository. + """ + setRepositoryInteractionLimit( + """ + Parameters for SetRepositoryInteractionLimit + """ + input: SetRepositoryInteractionLimitInput! + ): SetRepositoryInteractionLimitPayload + + """ + Set a user level interaction limit for an user's public repositories. + """ + setUserInteractionLimit( + """ + Parameters for SetUserInteractionLimit + """ + input: SetUserInteractionLimitInput! + ): SetUserInteractionLimitPayload + + """ + Starts a GitHub Enterprise Importer organization migration. + """ + startOrganizationMigration( + """ + Parameters for StartOrganizationMigration + """ + input: StartOrganizationMigrationInput! + ): StartOrganizationMigrationPayload + + """ + Starts a GitHub Enterprise Importer (GEI) repository migration. + """ + startRepositoryMigration( + """ + Parameters for StartRepositoryMigration + """ + input: StartRepositoryMigrationInput! + ): StartRepositoryMigrationPayload + + """ + Submits a pending pull request review. + """ + submitPullRequestReview( + """ + Parameters for SubmitPullRequestReview + """ + input: SubmitPullRequestReviewInput! + ): SubmitPullRequestReviewPayload + + """ + Transfer an organization from one enterprise to another enterprise. + """ + transferEnterpriseOrganization( + """ + Parameters for TransferEnterpriseOrganization + """ + input: TransferEnterpriseOrganizationInput! + ): TransferEnterpriseOrganizationPayload + + """ + Transfer an issue to a different repository + """ + transferIssue( + """ + Parameters for TransferIssue + """ + input: TransferIssueInput! + ): TransferIssuePayload + + """ + Unarchives a ProjectV2Item + """ + unarchiveProjectV2Item( + """ + Parameters for UnarchiveProjectV2Item + """ + input: UnarchiveProjectV2ItemInput! + ): UnarchiveProjectV2ItemPayload + + """ + Unarchives a repository. + """ + unarchiveRepository( + """ + Parameters for UnarchiveRepository + """ + input: UnarchiveRepositoryInput! + ): UnarchiveRepositoryPayload + + """ + Unfollow an organization. + """ + unfollowOrganization( + """ + Parameters for UnfollowOrganization + """ + input: UnfollowOrganizationInput! + ): UnfollowOrganizationPayload + + """ + Unfollow a user. + """ + unfollowUser( + """ + Parameters for UnfollowUser + """ + input: UnfollowUserInput! + ): UnfollowUserPayload + + """ + Unlinks a project from a repository. + """ + unlinkProjectV2FromRepository( + """ + Parameters for UnlinkProjectV2FromRepository + """ + input: UnlinkProjectV2FromRepositoryInput! + ): UnlinkProjectV2FromRepositoryPayload + + """ + Unlinks a project to a team. + """ + unlinkProjectV2FromTeam( + """ + Parameters for UnlinkProjectV2FromTeam + """ + input: UnlinkProjectV2FromTeamInput! + ): UnlinkProjectV2FromTeamPayload + + """ + Deletes a repository link from a project. + """ + unlinkRepositoryFromProject( + """ + Parameters for UnlinkRepositoryFromProject + """ + input: UnlinkRepositoryFromProjectInput! + ): UnlinkRepositoryFromProjectPayload + + """ + Unlock a lockable object + """ + unlockLockable( + """ + Parameters for UnlockLockable + """ + input: UnlockLockableInput! + ): UnlockLockablePayload + + """ + Unmark a discussion comment as the chosen answer for discussions in an answerable category. + """ + unmarkDiscussionCommentAsAnswer( + """ + Parameters for UnmarkDiscussionCommentAsAnswer + """ + input: UnmarkDiscussionCommentAsAnswerInput! + ): UnmarkDiscussionCommentAsAnswerPayload + + """ + Unmark a pull request file as viewed + """ + unmarkFileAsViewed( + """ + Parameters for UnmarkFileAsViewed + """ + input: UnmarkFileAsViewedInput! + ): UnmarkFileAsViewedPayload + + """ + Unmark an issue as a duplicate of another issue. + """ + unmarkIssueAsDuplicate( + """ + Parameters for UnmarkIssueAsDuplicate + """ + input: UnmarkIssueAsDuplicateInput! + ): UnmarkIssueAsDuplicatePayload + + """ + Unmark a project as a template. + """ + unmarkProjectV2AsTemplate( + """ + Parameters for UnmarkProjectV2AsTemplate + """ + input: UnmarkProjectV2AsTemplateInput! + ): UnmarkProjectV2AsTemplatePayload + + """ + Unminimizes a comment on an Issue, Commit, Pull Request, or Gist + """ + unminimizeComment( + """ + Parameters for UnminimizeComment + """ + input: UnminimizeCommentInput! + ): UnminimizeCommentPayload + + """ + Unpin a pinned issue from a repository + """ + unpinIssue( + """ + Parameters for UnpinIssue + """ + input: UnpinIssueInput! + ): UnpinIssuePayload + + """ + Marks a review thread as unresolved. + """ + unresolveReviewThread( + """ + Parameters for UnresolveReviewThread + """ + input: UnresolveReviewThreadInput! + ): UnresolveReviewThreadPayload + + """ + Update a branch protection rule + """ + updateBranchProtectionRule( + """ + Parameters for UpdateBranchProtectionRule + """ + input: UpdateBranchProtectionRuleInput! + ): UpdateBranchProtectionRulePayload + + """ + Update a check run + """ + updateCheckRun( + """ + Parameters for UpdateCheckRun + """ + input: UpdateCheckRunInput! + ): UpdateCheckRunPayload + + """ + Modifies the settings of an existing check suite + """ + updateCheckSuitePreferences( + """ + Parameters for UpdateCheckSuitePreferences + """ + input: UpdateCheckSuitePreferencesInput! + ): UpdateCheckSuitePreferencesPayload + + """ + Update a discussion + """ + updateDiscussion( + """ + Parameters for UpdateDiscussion + """ + input: UpdateDiscussionInput! + ): UpdateDiscussionPayload + + """ + Update the contents of a comment on a Discussion + """ + updateDiscussionComment( + """ + Parameters for UpdateDiscussionComment + """ + input: UpdateDiscussionCommentInput! + ): UpdateDiscussionCommentPayload + + """ + Updates the role of an enterprise administrator. + """ + updateEnterpriseAdministratorRole( + """ + Parameters for UpdateEnterpriseAdministratorRole + """ + input: UpdateEnterpriseAdministratorRoleInput! + ): UpdateEnterpriseAdministratorRolePayload + + """ + Sets whether private repository forks are enabled for an enterprise. + """ + updateEnterpriseAllowPrivateRepositoryForkingSetting( + """ + Parameters for UpdateEnterpriseAllowPrivateRepositoryForkingSetting + """ + input: UpdateEnterpriseAllowPrivateRepositoryForkingSettingInput! + ): UpdateEnterpriseAllowPrivateRepositoryForkingSettingPayload + + """ + Sets the base repository permission for organizations in an enterprise. + """ + updateEnterpriseDefaultRepositoryPermissionSetting( + """ + Parameters for UpdateEnterpriseDefaultRepositoryPermissionSetting + """ + input: UpdateEnterpriseDefaultRepositoryPermissionSettingInput! + ): UpdateEnterpriseDefaultRepositoryPermissionSettingPayload + + """ + Sets whether organization members with admin permissions on a repository can change repository visibility. + """ + updateEnterpriseMembersCanChangeRepositoryVisibilitySetting( + """ + Parameters for UpdateEnterpriseMembersCanChangeRepositoryVisibilitySetting + """ + input: UpdateEnterpriseMembersCanChangeRepositoryVisibilitySettingInput! + ): UpdateEnterpriseMembersCanChangeRepositoryVisibilitySettingPayload + + """ + Sets the members can create repositories setting for an enterprise. + """ + updateEnterpriseMembersCanCreateRepositoriesSetting( + """ + Parameters for UpdateEnterpriseMembersCanCreateRepositoriesSetting + """ + input: UpdateEnterpriseMembersCanCreateRepositoriesSettingInput! + ): UpdateEnterpriseMembersCanCreateRepositoriesSettingPayload + + """ + Sets the members can delete issues setting for an enterprise. + """ + updateEnterpriseMembersCanDeleteIssuesSetting( + """ + Parameters for UpdateEnterpriseMembersCanDeleteIssuesSetting + """ + input: UpdateEnterpriseMembersCanDeleteIssuesSettingInput! + ): UpdateEnterpriseMembersCanDeleteIssuesSettingPayload + + """ + Sets the members can delete repositories setting for an enterprise. + """ + updateEnterpriseMembersCanDeleteRepositoriesSetting( + """ + Parameters for UpdateEnterpriseMembersCanDeleteRepositoriesSetting + """ + input: UpdateEnterpriseMembersCanDeleteRepositoriesSettingInput! + ): UpdateEnterpriseMembersCanDeleteRepositoriesSettingPayload + + """ + Sets whether members can invite collaborators are enabled for an enterprise. + """ + updateEnterpriseMembersCanInviteCollaboratorsSetting( + """ + Parameters for UpdateEnterpriseMembersCanInviteCollaboratorsSetting + """ + input: UpdateEnterpriseMembersCanInviteCollaboratorsSettingInput! + ): UpdateEnterpriseMembersCanInviteCollaboratorsSettingPayload + + """ + Sets whether or not an organization admin can make purchases. + """ + updateEnterpriseMembersCanMakePurchasesSetting( + """ + Parameters for UpdateEnterpriseMembersCanMakePurchasesSetting + """ + input: UpdateEnterpriseMembersCanMakePurchasesSettingInput! + ): UpdateEnterpriseMembersCanMakePurchasesSettingPayload + + """ + Sets the members can update protected branches setting for an enterprise. + """ + updateEnterpriseMembersCanUpdateProtectedBranchesSetting( + """ + Parameters for UpdateEnterpriseMembersCanUpdateProtectedBranchesSetting + """ + input: UpdateEnterpriseMembersCanUpdateProtectedBranchesSettingInput! + ): UpdateEnterpriseMembersCanUpdateProtectedBranchesSettingPayload + + """ + Sets the members can view dependency insights for an enterprise. + """ + updateEnterpriseMembersCanViewDependencyInsightsSetting( + """ + Parameters for UpdateEnterpriseMembersCanViewDependencyInsightsSetting + """ + input: UpdateEnterpriseMembersCanViewDependencyInsightsSettingInput! + ): UpdateEnterpriseMembersCanViewDependencyInsightsSettingPayload + + """ + Sets whether organization projects are enabled for an enterprise. + """ + updateEnterpriseOrganizationProjectsSetting( + """ + Parameters for UpdateEnterpriseOrganizationProjectsSetting + """ + input: UpdateEnterpriseOrganizationProjectsSettingInput! + ): UpdateEnterpriseOrganizationProjectsSettingPayload + + """ + Updates the role of an enterprise owner with an organization. + """ + updateEnterpriseOwnerOrganizationRole( + """ + Parameters for UpdateEnterpriseOwnerOrganizationRole + """ + input: UpdateEnterpriseOwnerOrganizationRoleInput! + ): UpdateEnterpriseOwnerOrganizationRolePayload + + """ + Updates an enterprise's profile. + """ + updateEnterpriseProfile( + """ + Parameters for UpdateEnterpriseProfile + """ + input: UpdateEnterpriseProfileInput! + ): UpdateEnterpriseProfilePayload + + """ + Sets whether repository projects are enabled for a enterprise. + """ + updateEnterpriseRepositoryProjectsSetting( + """ + Parameters for UpdateEnterpriseRepositoryProjectsSetting + """ + input: UpdateEnterpriseRepositoryProjectsSettingInput! + ): UpdateEnterpriseRepositoryProjectsSettingPayload + + """ + Sets whether team discussions are enabled for an enterprise. + """ + updateEnterpriseTeamDiscussionsSetting( + """ + Parameters for UpdateEnterpriseTeamDiscussionsSetting + """ + input: UpdateEnterpriseTeamDiscussionsSettingInput! + ): UpdateEnterpriseTeamDiscussionsSettingPayload + + """ + Sets whether two factor authentication is required for all users in an enterprise. + """ + updateEnterpriseTwoFactorAuthenticationRequiredSetting( + """ + Parameters for UpdateEnterpriseTwoFactorAuthenticationRequiredSetting + """ + input: UpdateEnterpriseTwoFactorAuthenticationRequiredSettingInput! + ): UpdateEnterpriseTwoFactorAuthenticationRequiredSettingPayload + + """ + Updates an environment. + """ + updateEnvironment( + """ + Parameters for UpdateEnvironment + """ + input: UpdateEnvironmentInput! + ): UpdateEnvironmentPayload + + """ + Sets whether an IP allow list is enabled on an owner. + """ + updateIpAllowListEnabledSetting( + """ + Parameters for UpdateIpAllowListEnabledSetting + """ + input: UpdateIpAllowListEnabledSettingInput! + ): UpdateIpAllowListEnabledSettingPayload + + """ + Updates an IP allow list entry. + """ + updateIpAllowListEntry( + """ + Parameters for UpdateIpAllowListEntry + """ + input: UpdateIpAllowListEntryInput! + ): UpdateIpAllowListEntryPayload + + """ + Sets whether IP allow list configuration for installed GitHub Apps is enabled on an owner. + """ + updateIpAllowListForInstalledAppsEnabledSetting( + """ + Parameters for UpdateIpAllowListForInstalledAppsEnabledSetting + """ + input: UpdateIpAllowListForInstalledAppsEnabledSettingInput! + ): UpdateIpAllowListForInstalledAppsEnabledSettingPayload + + """ + Updates an Issue. + """ + updateIssue( + """ + Parameters for UpdateIssue + """ + input: UpdateIssueInput! + ): UpdateIssuePayload + + """ + Updates an IssueComment object. + """ + updateIssueComment( + """ + Parameters for UpdateIssueComment + """ + input: UpdateIssueCommentInput! + ): UpdateIssueCommentPayload + + """ + Updates an existing label. + """ + updateLabel( + """ + Parameters for UpdateLabel + """ + input: UpdateLabelInput! + ): UpdateLabelPayload @preview(toggledBy: "bane-preview") + + """ + Update the setting to restrict notifications to only verified or approved domains available to an owner. + """ + updateNotificationRestrictionSetting( + """ + Parameters for UpdateNotificationRestrictionSetting + """ + input: UpdateNotificationRestrictionSettingInput! + ): UpdateNotificationRestrictionSettingPayload + + """ + Sets whether private repository forks are enabled for an organization. + """ + updateOrganizationAllowPrivateRepositoryForkingSetting( + """ + Parameters for UpdateOrganizationAllowPrivateRepositoryForkingSetting + """ + input: UpdateOrganizationAllowPrivateRepositoryForkingSettingInput! + ): UpdateOrganizationAllowPrivateRepositoryForkingSettingPayload + + """ + Sets whether contributors are required to sign off on web-based commits for repositories in an organization. + """ + updateOrganizationWebCommitSignoffSetting( + """ + Parameters for UpdateOrganizationWebCommitSignoffSetting + """ + input: UpdateOrganizationWebCommitSignoffSettingInput! + ): UpdateOrganizationWebCommitSignoffSettingPayload + + """ + Updates an existing project. + """ + updateProject( + """ + Parameters for UpdateProject + """ + input: UpdateProjectInput! + ): UpdateProjectPayload + + """ + Updates an existing project card. + """ + updateProjectCard( + """ + Parameters for UpdateProjectCard + """ + input: UpdateProjectCardInput! + ): UpdateProjectCardPayload + + """ + Updates an existing project column. + """ + updateProjectColumn( + """ + Parameters for UpdateProjectColumn + """ + input: UpdateProjectColumnInput! + ): UpdateProjectColumnPayload + + """ + Updates an existing project (beta). + """ + updateProjectV2( + """ + Parameters for UpdateProjectV2 + """ + input: UpdateProjectV2Input! + ): UpdateProjectV2Payload + + """ + Update the collaborators on a team or a project + """ + updateProjectV2Collaborators( + """ + Parameters for UpdateProjectV2Collaborators + """ + input: UpdateProjectV2CollaboratorsInput! + ): UpdateProjectV2CollaboratorsPayload + + """ + Updates a draft issue within a Project. + """ + updateProjectV2DraftIssue( + """ + Parameters for UpdateProjectV2DraftIssue + """ + input: UpdateProjectV2DraftIssueInput! + ): UpdateProjectV2DraftIssuePayload + + """ + This mutation updates the value of a field for an item in a Project. Currently + only single-select, text, number, date, and iteration fields are supported. + """ + updateProjectV2ItemFieldValue( + """ + Parameters for UpdateProjectV2ItemFieldValue + """ + input: UpdateProjectV2ItemFieldValueInput! + ): UpdateProjectV2ItemFieldValuePayload + + """ + This mutation updates the position of the item in the project, where the position represents the priority of an item. + """ + updateProjectV2ItemPosition( + """ + Parameters for UpdateProjectV2ItemPosition + """ + input: UpdateProjectV2ItemPositionInput! + ): UpdateProjectV2ItemPositionPayload + + """ + Update a pull request + """ + updatePullRequest( + """ + Parameters for UpdatePullRequest + """ + input: UpdatePullRequestInput! + ): UpdatePullRequestPayload + + """ + Merge or Rebase HEAD from upstream branch into pull request branch + """ + updatePullRequestBranch( + """ + Parameters for UpdatePullRequestBranch + """ + input: UpdatePullRequestBranchInput! + ): UpdatePullRequestBranchPayload + + """ + Updates the body of a pull request review. + """ + updatePullRequestReview( + """ + Parameters for UpdatePullRequestReview + """ + input: UpdatePullRequestReviewInput! + ): UpdatePullRequestReviewPayload + + """ + Updates a pull request review comment. + """ + updatePullRequestReviewComment( + """ + Parameters for UpdatePullRequestReviewComment + """ + input: UpdatePullRequestReviewCommentInput! + ): UpdatePullRequestReviewCommentPayload + + """ + Update a Git Ref. + """ + updateRef( + """ + Parameters for UpdateRef + """ + input: UpdateRefInput! + ): UpdateRefPayload + + """ + Creates, updates and/or deletes multiple refs in a repository. + + This mutation takes a list of `RefUpdate`s and performs these updates + on the repository. All updates are performed atomically, meaning that + if one of them is rejected, no other ref will be modified. + + `RefUpdate.beforeOid` specifies that the given reference needs to point + to the given value before performing any updates. A value of + `0000000000000000000000000000000000000000` can be used to verify that + the references should not exist. + + `RefUpdate.afterOid` specifies the value that the given reference + will point to after performing all updates. A value of + `0000000000000000000000000000000000000000` can be used to delete a + reference. + + If `RefUpdate.force` is set to `true`, a non-fast-forward updates + for the given reference will be allowed. + """ + updateRefs( + """ + Parameters for UpdateRefs + """ + input: UpdateRefsInput! + ): UpdateRefsPayload @preview(toggledBy: "update-refs-preview") + + """ + Update information about a repository. + """ + updateRepository( + """ + Parameters for UpdateRepository + """ + input: UpdateRepositoryInput! + ): UpdateRepositoryPayload + + """ + Update a repository ruleset + """ + updateRepositoryRuleset( + """ + Parameters for UpdateRepositoryRuleset + """ + input: UpdateRepositoryRulesetInput! + ): UpdateRepositoryRulesetPayload + + """ + Sets whether contributors are required to sign off on web-based commits for a repository. + """ + updateRepositoryWebCommitSignoffSetting( + """ + Parameters for UpdateRepositoryWebCommitSignoffSetting + """ + input: UpdateRepositoryWebCommitSignoffSettingInput! + ): UpdateRepositoryWebCommitSignoffSettingPayload + + """ + Change visibility of your sponsorship and opt in or out of email updates from the maintainer. + """ + updateSponsorshipPreferences( + """ + Parameters for UpdateSponsorshipPreferences + """ + input: UpdateSponsorshipPreferencesInput! + ): UpdateSponsorshipPreferencesPayload + + """ + Updates the state for subscribable subjects. + """ + updateSubscription( + """ + Parameters for UpdateSubscription + """ + input: UpdateSubscriptionInput! + ): UpdateSubscriptionPayload + + """ + Updates a team discussion. + """ + updateTeamDiscussion( + """ + Parameters for UpdateTeamDiscussion + """ + input: UpdateTeamDiscussionInput! + ): UpdateTeamDiscussionPayload + + """ + Updates a discussion comment. + """ + updateTeamDiscussionComment( + """ + Parameters for UpdateTeamDiscussionComment + """ + input: UpdateTeamDiscussionCommentInput! + ): UpdateTeamDiscussionCommentPayload + + """ + Updates team review assignment. + """ + updateTeamReviewAssignment( + """ + Parameters for UpdateTeamReviewAssignment + """ + input: UpdateTeamReviewAssignmentInput! + ): UpdateTeamReviewAssignmentPayload @preview(toggledBy: "stone-crop-preview") + + """ + Update team repository. + """ + updateTeamsRepository( + """ + Parameters for UpdateTeamsRepository + """ + input: UpdateTeamsRepositoryInput! + ): UpdateTeamsRepositoryPayload + + """ + Replaces the repository's topics with the given topics. + """ + updateTopics( + """ + Parameters for UpdateTopics + """ + input: UpdateTopicsInput! + ): UpdateTopicsPayload + + """ + Verify that a verifiable domain has the expected DNS record. + """ + verifyVerifiableDomain( + """ + Parameters for VerifyVerifiableDomain + """ + input: VerifyVerifiableDomainInput! + ): VerifyVerifiableDomainPayload +} + +""" +An object with an ID. +""" +interface Node { + """ + ID of the object. + """ + id: ID! +} + +""" +The possible values for the notification restriction setting. +""" +enum NotificationRestrictionSettingValue { + """ + The setting is disabled for the owner. + """ + DISABLED + + """ + The setting is enabled for the owner. + """ + ENABLED +} + +""" +An OIDC identity provider configured to provision identities for an enterprise. +Visible to enterprise owners or enterprise owners' personal access tokens +(classic) with read:enterprise or admin:enterprise scope. +""" +type OIDCProvider implements Node { + """ + The enterprise this identity provider belongs to. + """ + enterprise: Enterprise + + """ + ExternalIdentities provisioned by this identity provider. + """ + externalIdentities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter to external identities with the users login + """ + login: String + + """ + Filter to external identities with valid org membership only + """ + membersOnly: Boolean + + """ + Filter to external identities with the users userName/NameID attribute + """ + userName: String + ): ExternalIdentityConnection! + id: ID! + + """ + The OIDC identity provider type + """ + providerType: OIDCProviderType! + + """ + The id of the tenant this provider is attached to + """ + tenantId: String! +} + +""" +The OIDC identity provider type +""" +enum OIDCProviderType { + """ + Azure Active Directory + """ + AAD +} + +""" +Metadata for an audit entry with action oauth_application.* +""" +interface OauthApplicationAuditEntryData { + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI +} + +""" +Audit log entry for a oauth_application.create event. +""" +type OauthApplicationCreateAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The application URL of the OAuth application. + """ + applicationUrl: URI + + """ + The callback URL of the OAuth application. + """ + callbackUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The rate limit of the OAuth application. + """ + rateLimit: Int + + """ + The state of the OAuth application. + """ + state: OauthApplicationCreateAuditEntryState + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The state of an OAuth application when it was created. +""" +enum OauthApplicationCreateAuditEntryState { + """ + The OAuth application was active and allowed to have OAuth Accesses. + """ + ACTIVE + + """ + The OAuth application was in the process of being deleted. + """ + PENDING_DELETION + + """ + The OAuth application was suspended from generating OAuth Accesses due to abuse or security concerns. + """ + SUSPENDED +} + +""" +The corresponding operation type for the action +""" +enum OperationType { + """ + An existing resource was accessed + """ + ACCESS + + """ + A resource performed an authentication event + """ + AUTHENTICATION + + """ + A new resource was created + """ + CREATE + + """ + An existing resource was modified + """ + MODIFY + + """ + An existing resource was removed + """ + REMOVE + + """ + An existing resource was restored + """ + RESTORE + + """ + An existing resource was transferred between multiple resources + """ + TRANSFER +} + +""" +Possible directions in which to order a list of items when provided an `orderBy` argument. +""" +enum OrderDirection { + """ + Specifies an ascending order for a given `orderBy` argument. + """ + ASC + + """ + Specifies a descending order for a given `orderBy` argument. + """ + DESC +} + +""" +Audit log entry for a org.add_billing_manager +""" +type OrgAddBillingManagerAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The email address used to invite a billing manager for the organization. + """ + invitationEmail: String + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.add_member +""" +type OrgAddMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The permission level of the member added to the organization. + """ + permission: OrgAddMemberAuditEntryPermission + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The permissions available to members on an Organization. +""" +enum OrgAddMemberAuditEntryPermission { + """ + Can read, clone, push, and add collaborators to repositories. + """ + ADMIN + + """ + Can read and clone repositories. + """ + READ +} + +""" +Audit log entry for a org.block_user +""" +type OrgBlockUserAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The blocked user. + """ + blockedUser: User + + """ + The username of the blocked user. + """ + blockedUserName: String + + """ + The HTTP path for the blocked user. + """ + blockedUserResourcePath: URI + + """ + The HTTP URL for the blocked user. + """ + blockedUserUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.config.disable_collaborators_only event. +""" +type OrgConfigDisableCollaboratorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.config.enable_collaborators_only event. +""" +type OrgConfigEnableCollaboratorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.create event. +""" +type OrgCreateAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The billing plan for the Organization. + """ + billingPlan: OrgCreateAuditEntryBillingPlan + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The billing plans available for organizations. +""" +enum OrgCreateAuditEntryBillingPlan { + """ + Team Plan + """ + BUSINESS + + """ + Enterprise Cloud Plan + """ + BUSINESS_PLUS + + """ + Free Plan + """ + FREE + + """ + Tiered Per Seat Plan + """ + TIERED_PER_SEAT + + """ + Legacy Unlimited Plan + """ + UNLIMITED +} + +""" +Audit log entry for a org.disable_oauth_app_restrictions event. +""" +type OrgDisableOauthAppRestrictionsAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.disable_saml event. +""" +type OrgDisableSamlAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The SAML provider's digest algorithm URL. + """ + digestMethodUrl: URI + id: ID! + + """ + The SAML provider's issuer URL. + """ + issuerUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The SAML provider's signature algorithm URL. + """ + signatureMethodUrl: URI + + """ + The SAML provider's single sign-on URL. + """ + singleSignOnUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.disable_two_factor_requirement event. +""" +type OrgDisableTwoFactorRequirementAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.enable_oauth_app_restrictions event. +""" +type OrgEnableOauthAppRestrictionsAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.enable_saml event. +""" +type OrgEnableSamlAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The SAML provider's digest algorithm URL. + """ + digestMethodUrl: URI + id: ID! + + """ + The SAML provider's issuer URL. + """ + issuerUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The SAML provider's signature algorithm URL. + """ + signatureMethodUrl: URI + + """ + The SAML provider's single sign-on URL. + """ + singleSignOnUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.enable_two_factor_requirement event. +""" +type OrgEnableTwoFactorRequirementAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Ordering options for an organization's enterprise owner connections. +""" +input OrgEnterpriseOwnerOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order enterprise owners by. + """ + field: OrgEnterpriseOwnerOrderField! +} + +""" +Properties by which enterprise owners can be ordered. +""" +enum OrgEnterpriseOwnerOrderField { + """ + Order enterprise owners by login. + """ + LOGIN +} + +""" +Audit log entry for a org.invite_member event. +""" +type OrgInviteMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The email address of the organization invitation. + """ + email: String + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The organization invitation. + """ + organizationInvitation: OrganizationInvitation + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.invite_to_business event. +""" +type OrgInviteToBusinessAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.oauth_app_access_approved event. +""" +type OrgOauthAppAccessApprovedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.oauth_app_access_blocked event. +""" +type OrgOauthAppAccessBlockedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.oauth_app_access_denied event. +""" +type OrgOauthAppAccessDeniedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.oauth_app_access_requested event. +""" +type OrgOauthAppAccessRequestedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.oauth_app_access_unblocked event. +""" +type OrgOauthAppAccessUnblockedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.remove_billing_manager event. +""" +type OrgRemoveBillingManagerAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The reason for the billing manager being removed. + """ + reason: OrgRemoveBillingManagerAuditEntryReason + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The reason a billing manager was removed from an Organization. +""" +enum OrgRemoveBillingManagerAuditEntryReason { + """ + SAML external identity missing + """ + SAML_EXTERNAL_IDENTITY_MISSING + + """ + SAML SSO enforcement requires an external identity + """ + SAML_SSO_ENFORCEMENT_REQUIRES_EXTERNAL_IDENTITY + + """ + The organization required 2FA of its billing managers and this user did not have 2FA enabled. + """ + TWO_FACTOR_REQUIREMENT_NON_COMPLIANCE +} + +""" +Audit log entry for a org.remove_member event. +""" +type OrgRemoveMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The types of membership the member has with the organization. + """ + membershipTypes: [OrgRemoveMemberAuditEntryMembershipType!] + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The reason for the member being removed. + """ + reason: OrgRemoveMemberAuditEntryReason + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The type of membership a user has with an Organization. +""" +enum OrgRemoveMemberAuditEntryMembershipType { + """ + Organization administrators have full access and can change several settings, + including the names of repositories that belong to the Organization and Owners + team membership. In addition, organization admins can delete the organization + and all of its repositories. + """ + ADMIN + + """ + A billing manager is a user who manages the billing settings for the Organization, such as updating payment information. + """ + BILLING_MANAGER + + """ + A direct member is a user that is a member of the Organization. + """ + DIRECT_MEMBER + + """ + An outside collaborator is a person who isn't explicitly a member of the + Organization, but who has Read, Write, or Admin permissions to one or more + repositories in the organization. + """ + OUTSIDE_COLLABORATOR + + """ + A suspended member. + """ + SUSPENDED + + """ + An unaffiliated collaborator is a person who is not a member of the + Organization and does not have access to any repositories in the Organization. + """ + UNAFFILIATED +} + +""" +The reason a member was removed from an Organization. +""" +enum OrgRemoveMemberAuditEntryReason { + """ + SAML external identity missing + """ + SAML_EXTERNAL_IDENTITY_MISSING + + """ + SAML SSO enforcement requires an external identity + """ + SAML_SSO_ENFORCEMENT_REQUIRES_EXTERNAL_IDENTITY + + """ + User was removed from organization during account recovery + """ + TWO_FACTOR_ACCOUNT_RECOVERY + + """ + The organization required 2FA of its billing managers and this user did not have 2FA enabled. + """ + TWO_FACTOR_REQUIREMENT_NON_COMPLIANCE + + """ + User account has been deleted + """ + USER_ACCOUNT_DELETED +} + +""" +Audit log entry for a org.remove_outside_collaborator event. +""" +type OrgRemoveOutsideCollaboratorAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The types of membership the outside collaborator has with the organization. + """ + membershipTypes: [OrgRemoveOutsideCollaboratorAuditEntryMembershipType!] + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The reason for the outside collaborator being removed from the Organization. + """ + reason: OrgRemoveOutsideCollaboratorAuditEntryReason + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The type of membership a user has with an Organization. +""" +enum OrgRemoveOutsideCollaboratorAuditEntryMembershipType { + """ + A billing manager is a user who manages the billing settings for the Organization, such as updating payment information. + """ + BILLING_MANAGER + + """ + An outside collaborator is a person who isn't explicitly a member of the + Organization, but who has Read, Write, or Admin permissions to one or more + repositories in the organization. + """ + OUTSIDE_COLLABORATOR + + """ + An unaffiliated collaborator is a person who is not a member of the + Organization and does not have access to any repositories in the organization. + """ + UNAFFILIATED +} + +""" +The reason an outside collaborator was removed from an Organization. +""" +enum OrgRemoveOutsideCollaboratorAuditEntryReason { + """ + SAML external identity missing + """ + SAML_EXTERNAL_IDENTITY_MISSING + + """ + The organization required 2FA of its billing managers and this user did not have 2FA enabled. + """ + TWO_FACTOR_REQUIREMENT_NON_COMPLIANCE +} + +""" +Audit log entry for a org.restore_member event. +""" +type OrgRestoreMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The number of custom email routings for the restored member. + """ + restoredCustomEmailRoutingsCount: Int + + """ + The number of issue assignments for the restored member. + """ + restoredIssueAssignmentsCount: Int + + """ + Restored organization membership objects. + """ + restoredMemberships: [OrgRestoreMemberAuditEntryMembership!] + + """ + The number of restored memberships. + """ + restoredMembershipsCount: Int + + """ + The number of repositories of the restored member. + """ + restoredRepositoriesCount: Int + + """ + The number of starred repositories for the restored member. + """ + restoredRepositoryStarsCount: Int + + """ + The number of watched repositories for the restored member. + """ + restoredRepositoryWatchesCount: Int + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Types of memberships that can be restored for an Organization member. +""" +union OrgRestoreMemberAuditEntryMembership = + OrgRestoreMemberMembershipOrganizationAuditEntryData + | OrgRestoreMemberMembershipRepositoryAuditEntryData + | OrgRestoreMemberMembershipTeamAuditEntryData + +""" +Metadata for an organization membership for org.restore_member actions +""" +type OrgRestoreMemberMembershipOrganizationAuditEntryData implements OrganizationAuditEntryData { + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI +} + +""" +Metadata for a repository membership for org.restore_member actions +""" +type OrgRestoreMemberMembershipRepositoryAuditEntryData implements RepositoryAuditEntryData { + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI +} + +""" +Metadata for a team membership for org.restore_member actions +""" +type OrgRestoreMemberMembershipTeamAuditEntryData implements TeamAuditEntryData { + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI +} + +""" +Audit log entry for a org.unblock_user +""" +type OrgUnblockUserAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The user being unblocked by the organization. + """ + blockedUser: User + + """ + The username of the blocked user. + """ + blockedUserName: String + + """ + The HTTP path for the blocked user. + """ + blockedUserResourcePath: URI + + """ + The HTTP URL for the blocked user. + """ + blockedUserUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.update_default_repository_permission +""" +type OrgUpdateDefaultRepositoryPermissionAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The new base repository permission level for the organization. + """ + permission: OrgUpdateDefaultRepositoryPermissionAuditEntryPermission + + """ + The former base repository permission level for the organization. + """ + permissionWas: OrgUpdateDefaultRepositoryPermissionAuditEntryPermission + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The default permission a repository can have in an Organization. +""" +enum OrgUpdateDefaultRepositoryPermissionAuditEntryPermission { + """ + Can read, clone, push, and add collaborators to repositories. + """ + ADMIN + + """ + No default permission value. + """ + NONE + + """ + Can read and clone repositories. + """ + READ + + """ + Can read, clone and push to repositories. + """ + WRITE +} + +""" +Audit log entry for a org.update_member event. +""" +type OrgUpdateMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The new member permission level for the organization. + """ + permission: OrgUpdateMemberAuditEntryPermission + + """ + The former member permission level for the organization. + """ + permissionWas: OrgUpdateMemberAuditEntryPermission + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The permissions available to members on an Organization. +""" +enum OrgUpdateMemberAuditEntryPermission { + """ + Can read, clone, push, and add collaborators to repositories. + """ + ADMIN + + """ + Can read and clone repositories. + """ + READ +} + +""" +Audit log entry for a org.update_member_repository_creation_permission event. +""" +type OrgUpdateMemberRepositoryCreationPermissionAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + Can members create repositories in the organization. + """ + canCreateRepositories: Boolean + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The permission for visibility level of repositories for this organization. + """ + visibility: OrgUpdateMemberRepositoryCreationPermissionAuditEntryVisibility +} + +""" +The permissions available for repository creation on an Organization. +""" +enum OrgUpdateMemberRepositoryCreationPermissionAuditEntryVisibility { + """ + All organization members are restricted from creating any repositories. + """ + ALL + + """ + All organization members are restricted from creating internal repositories. + """ + INTERNAL + + """ + All organization members are allowed to create any repositories. + """ + NONE + + """ + All organization members are restricted from creating private repositories. + """ + PRIVATE + + """ + All organization members are restricted from creating private or internal repositories. + """ + PRIVATE_INTERNAL + + """ + All organization members are restricted from creating public repositories. + """ + PUBLIC + + """ + All organization members are restricted from creating public or internal repositories. + """ + PUBLIC_INTERNAL + + """ + All organization members are restricted from creating public or private repositories. + """ + PUBLIC_PRIVATE +} + +""" +Audit log entry for a org.update_member_repository_invitation_permission event. +""" +type OrgUpdateMemberRepositoryInvitationPermissionAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + Can outside collaborators be invited to repositories in the organization. + """ + canInviteOutsideCollaboratorsToRepositories: Boolean + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +An account on GitHub, with one or more owners, that has repositories, members and teams. +""" +type Organization implements Actor & AnnouncementBanner & MemberStatusable & Node & PackageOwner & ProfileOwner & ProjectOwner & ProjectV2Owner & ProjectV2Recent & RepositoryDiscussionAuthor & RepositoryDiscussionCommentAuthor & RepositoryOwner & Sponsorable & UniformResourceLocatable { + """ + The text of the announcement + """ + announcement: String + + """ + The expiration date of the announcement, if any + """ + announcementExpiresAt: DateTime + + """ + Whether the announcement can be dismissed by the user + """ + announcementUserDismissible: Boolean + + """ + Determine if this repository owner has any items that can be pinned to their profile. + """ + anyPinnableItems( + """ + Filter to only a particular kind of pinnable item. + """ + type: PinnableItemType + ): Boolean! + + """ + Audit log entries of the organization + """ + auditLog( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the returned audit log entries. + """ + orderBy: AuditLogOrder = {field: CREATED_AT, direction: DESC} + + """ + The query string to filter audit entries + """ + query: String + ): OrganizationAuditEntryConnection! + + """ + A URL pointing to the organization's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The organization's public profile description. + """ + description: String + + """ + The organization's public profile description rendered to HTML. + """ + descriptionHTML: String + + """ + A list of domains owned by the organization. + """ + domains( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter by if the domain is approved. + """ + isApproved: Boolean = null + + """ + Filter by if the domain is verified. + """ + isVerified: Boolean = null + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for verifiable domains returned. + """ + orderBy: VerifiableDomainOrder = {field: DOMAIN, direction: ASC} + ): VerifiableDomainConnection + + """ + The organization's public email. + """ + email: String + + """ + A list of owners of the organization's enterprise account. + """ + enterpriseOwners( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for enterprise owners returned from the connection. + """ + orderBy: OrgEnterpriseOwnerOrder = {field: LOGIN, direction: ASC} + + """ + The organization role to filter by. + """ + organizationRole: RoleInOrganization + + """ + The search string to look for. + """ + query: String + ): OrganizationEnterpriseOwnerConnection! + + """ + The estimated next GitHub Sponsors payout for this user/organization in cents (USD). + """ + estimatedNextSponsorsPayoutInCents: Int! + + """ + True if this user/organization has a GitHub Sponsors listing. + """ + hasSponsorsListing: Boolean! + id: ID! + + """ + The interaction ability settings for this organization. + """ + interactionAbility: RepositoryInteractionAbility + + """ + The setting value for whether the organization has an IP allow list enabled. + """ + ipAllowListEnabledSetting: IpAllowListEnabledSettingValue! + + """ + The IP addresses that are allowed to access resources owned by the organization. + """ + ipAllowListEntries( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for IP allow list entries returned. + """ + orderBy: IpAllowListEntryOrder = {field: ALLOW_LIST_VALUE, direction: ASC} + ): IpAllowListEntryConnection! + + """ + The setting value for whether the organization has IP allow list configuration for installed GitHub Apps enabled. + """ + ipAllowListForInstalledAppsEnabledSetting: IpAllowListForInstalledAppsEnabledSettingValue! + + """ + Whether the given account is sponsoring this user/organization. + """ + isSponsoredBy( + """ + The target account's login. + """ + accountLogin: String! + ): Boolean! + + """ + True if the viewer is sponsored by this user/organization. + """ + isSponsoringViewer: Boolean! + + """ + Whether the organization has verified its profile email and website. + """ + isVerified: Boolean! + + """ + Showcases a selection of repositories and gists that the profile owner has + either curated or that have been selected automatically based on popularity. + """ + itemShowcase: ProfileItemShowcase! + + """ + The organization's public profile location. + """ + location: String + + """ + The organization's login name. + """ + login: String! + + """ + A list of all mannequins for this organization. + """ + mannequins( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter mannequins by login. + """ + login: String + + """ + Ordering options for mannequins returned from the connection. + """ + orderBy: MannequinOrder = {field: CREATED_AT, direction: ASC} + ): MannequinConnection! + + """ + Get the status messages members of this entity have set that are either public or visible only to the organization. + """ + memberStatuses( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for user statuses returned from the connection. + """ + orderBy: UserStatusOrder = {field: UPDATED_AT, direction: DESC} + ): UserStatusConnection! + + """ + Members can fork private repositories in this organization + """ + membersCanForkPrivateRepositories: Boolean! + + """ + A list of users who are members of this organization. + """ + membersWithRole( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationMemberConnection! + + """ + The estimated monthly GitHub Sponsors income for this user/organization in cents (USD). + """ + monthlyEstimatedSponsorsIncomeInCents: Int! + + """ + The organization's public profile name. + """ + name: String + + """ + The HTTP path creating a new team + """ + newTeamResourcePath: URI! + + """ + The HTTP URL creating a new team + """ + newTeamUrl: URI! + + """ + Indicates if email notification delivery for this organization is restricted to verified or approved domains. + """ + notificationDeliveryRestrictionEnabledSetting: NotificationRestrictionSettingValue! + + """ + The billing email for the organization. + """ + organizationBillingEmail: String + + """ + A list of packages under the owner. + """ + packages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Find packages by their names. + """ + names: [String] + + """ + Ordering of the returned packages. + """ + orderBy: PackageOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter registry package by type. + """ + packageType: PackageType + + """ + Find packages in a repository by ID. + """ + repositoryId: ID + ): PackageConnection! + + """ + A list of users who have been invited to join this organization. + """ + pendingMembers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + A list of repositories and gists this profile owner can pin to their profile. + """ + pinnableItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinnable items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + A list of repositories and gists this profile owner has pinned to their profile + """ + pinnedItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinned items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + Returns how many more items this profile owner can pin to their profile. + """ + pinnedItemsRemaining: Int! + + """ + Find project by number. + """ + project( + """ + The project number to find. + """ + number: Int! + ): Project + + """ + Find a project by number. + """ + projectV2( + """ + The project number. + """ + number: Int! + ): ProjectV2 + + """ + A list of projects under the owner. + """ + projects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for projects returned from the connection + """ + orderBy: ProjectOrder + + """ + Query to search projects by, currently only searching by name. + """ + search: String + + """ + A list of states to filter the projects by. + """ + states: [ProjectState!] + ): ProjectConnection! + + """ + The HTTP path listing organization's projects + """ + projectsResourcePath: URI! + + """ + The HTTP URL listing organization's projects + """ + projectsUrl: URI! + + """ + A list of projects under the owner. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + A project to search for under the the owner. + """ + query: String + ): ProjectV2Connection! + + """ + Recent projects that this user has modified in the context of the owner. + """ + recentProjects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2Connection! + + """ + A list of repositories that the user owns. + """ + repositories( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssuesEnabled: Boolean + + """ + If non-null, filters repositories according to whether they are archived and not maintained + """ + isArchived: Boolean + + """ + If non-null, filters repositories according to whether they are forks of another repository + """ + isFork: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + Find Repository. + """ + repository( + """ + Follow repository renames. If disabled, a repository referenced by its old name will return an error. + """ + followRenames: Boolean = true + + """ + Name of Repository to find. + """ + name: String! + ): Repository + + """ + Discussion comments this user has authored. + """ + repositoryDiscussionComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter discussion comments to only those that were marked as the answer + """ + onlyAnswers: Boolean = false + + """ + Filter discussion comments to only those in a specific repository. + """ + repositoryId: ID + ): DiscussionCommentConnection! + + """ + Discussions this user has started. + """ + repositoryDiscussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Filter discussions to only those that have been answered or not. Defaults to + including both answered and unanswered discussions. + """ + answered: Boolean = null + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for discussions returned from the connection. + """ + orderBy: DiscussionOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter discussions to only those in a specific repository. + """ + repositoryId: ID + + """ + A list of states to filter the discussions by. + """ + states: [DiscussionState!] = [] + ): DiscussionConnection! + + """ + A list of all repository migrations for this organization. + """ + repositoryMigrations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repository migrations returned. + """ + orderBy: RepositoryMigrationOrder = {field: CREATED_AT, direction: ASC} + + """ + Filter repository migrations by repository name. + """ + repositoryName: String + + """ + Filter repository migrations by state. + """ + state: MigrationState + ): RepositoryMigrationConnection! + + """ + When true the organization requires all members, billing managers, and outside + collaborators to enable two-factor authentication. + """ + requiresTwoFactorAuthentication: Boolean + + """ + The HTTP path for this organization. + """ + resourcePath: URI! + + """ + Returns a single ruleset from the current organization by ID. + """ + ruleset( + """ + The ID of the ruleset to be returned. + """ + databaseId: Int! + ): RepositoryRuleset + + """ + A list of rulesets for this organization. + """ + rulesets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Return rulesets configured at higher levels that apply to this organization + """ + includeParents: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RepositoryRulesetConnection + + """ + The Organization's SAML identity provider. Visible to (1) organization owners, + (2) organization owners' personal access tokens (classic) with read:org or + admin:org scope, (3) GitHub App with an installation token with read or write + access to members. + """ + samlIdentityProvider: OrganizationIdentityProvider + + """ + List of users and organizations this entity is sponsoring. + """ + sponsoring( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the users and organizations returned from the connection. + """ + orderBy: SponsorOrder = {field: RELEVANCE, direction: DESC} + ): SponsorConnection! + + """ + List of sponsors for this user or organization. + """ + sponsors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsors returned from the connection. + """ + orderBy: SponsorOrder = {field: RELEVANCE, direction: DESC} + + """ + If given, will filter for sponsors at the given tier. Will only return + sponsors whose tier the viewer is permitted to see. + """ + tierId: ID + ): SponsorConnection! + + """ + Events involving this sponsorable, such as new sponsorships. + """ + sponsorsActivities( + """ + Filter activities to only the specified actions. + """ + actions: [SponsorsActivityAction!] = [] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether to include those events where this sponsorable acted as the sponsor. + Defaults to only including events where this sponsorable was the recipient + of a sponsorship. + """ + includeAsSponsor: Boolean = false + + """ + Whether or not to include private activities in the result set. Defaults to including public and private activities. + """ + includePrivate: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for activity returned from the connection. + """ + orderBy: SponsorsActivityOrder = {field: TIMESTAMP, direction: DESC} + + """ + Filter activities returned to only those that occurred in the most recent + specified time period. Set to ALL to avoid filtering by when the activity + occurred. Will be ignored if `since` or `until` is given. + """ + period: SponsorsActivityPeriod = MONTH + + """ + Filter activities to those that occurred on or after this time. + """ + since: DateTime + + """ + Filter activities to those that occurred before this time. + """ + until: DateTime + ): SponsorsActivityConnection! + + """ + The GitHub Sponsors listing for this user or organization. + """ + sponsorsListing: SponsorsListing + + """ + The sponsorship from the viewer to this user/organization; that is, the sponsorship where you're the sponsor. + """ + sponsorshipForViewerAsSponsor( + """ + Whether to return the sponsorship only if it's still active. Pass false to + get the viewer's sponsorship back even if it has been cancelled. + """ + activeOnly: Boolean = true + ): Sponsorship + + """ + The sponsorship from this user/organization to the viewer; that is, the sponsorship you're receiving. + """ + sponsorshipForViewerAsSponsorable( + """ + Whether to return the sponsorship only if it's still active. Pass false to + get the sponsorship back even if it has been cancelled. + """ + activeOnly: Boolean = true + ): Sponsorship + + """ + List of sponsorship updates sent from this sponsorable to sponsors. + """ + sponsorshipNewsletters( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorship updates returned from the connection. + """ + orderBy: SponsorshipNewsletterOrder = {field: CREATED_AT, direction: DESC} + ): SponsorshipNewsletterConnection! + + """ + The sponsorships where this user or organization is the maintainer receiving the funds. + """ + sponsorshipsAsMaintainer( + """ + Whether to include only sponsorships that are active right now, versus all + sponsorships this maintainer has ever received. + """ + activeOnly: Boolean = true + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether or not to include private sponsorships in the result set + """ + includePrivate: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + The sponsorships where this user or organization is the funder. + """ + sponsorshipsAsSponsor( + """ + Whether to include only sponsorships that are active right now, versus all sponsorships this sponsor has ever made. + """ + activeOnly: Boolean = true + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter sponsorships returned to those for the specified maintainers. That + is, the recipient of the sponsorship is a user or organization with one of + the given logins. + """ + maintainerLogins: [String!] + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + Find an organization's team by its slug. + """ + team( + """ + The name or slug of the team to find. + """ + slug: String! + ): Team + + """ + A list of teams in this organization. + """ + teams( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + If true, filters teams that are mapped to an LDAP Group (Enterprise only) + """ + ldapMapped: Boolean + + """ + If non-null, filters teams according to notification setting + """ + notificationSetting: TeamNotificationSetting + + """ + Ordering options for teams returned from the connection + """ + orderBy: TeamOrder + + """ + If non-null, filters teams according to privacy + """ + privacy: TeamPrivacy + + """ + If non-null, filters teams with query on team name and team slug + """ + query: String + + """ + If non-null, filters teams according to whether the viewer is an admin or member on team + """ + role: TeamRole + + """ + If true, restrict to only root teams + """ + rootTeamsOnly: Boolean = false + + """ + User logins to filter by + """ + userLogins: [String!] + ): TeamConnection! + + """ + The HTTP path listing organization's teams + """ + teamsResourcePath: URI! + + """ + The HTTP URL listing organization's teams + """ + teamsUrl: URI! + + """ + The amount in United States cents (e.g., 500 = $5.00 USD) that this entity has + spent on GitHub to fund sponsorships. Only returns a value when viewed by the + user themselves or by a user who can manage sponsorships for the requested organization. + """ + totalSponsorshipAmountAsSponsorInCents( + """ + Filter payments to those that occurred on or after this time. + """ + since: DateTime + + """ + Filter payments to those made to the users or organizations with the specified usernames. + """ + sponsorableLogins: [String!] = [] + + """ + Filter payments to those that occurred before this time. + """ + until: DateTime + ): Int + + """ + The organization's Twitter username. + """ + twitterUsername: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this organization. + """ + url: URI! + + """ + Organization is adminable by the viewer. + """ + viewerCanAdminister: Boolean! + + """ + Can the viewer pin repositories and gists to the profile? + """ + viewerCanChangePinnedItems: Boolean! + + """ + Can the current viewer create new projects on this owner. + """ + viewerCanCreateProjects: Boolean! + + """ + Viewer can create repositories on this organization + """ + viewerCanCreateRepositories: Boolean! + + """ + Viewer can create teams on this organization. + """ + viewerCanCreateTeams: Boolean! + + """ + Whether or not the viewer is able to sponsor this user/organization. + """ + viewerCanSponsor: Boolean! + + """ + Viewer is an active member of this organization. + """ + viewerIsAMember: Boolean! + + """ + Whether or not this Organization is followed by the viewer. + """ + viewerIsFollowing: Boolean! + + """ + True if the viewer is sponsoring this user/organization. + """ + viewerIsSponsoring: Boolean! + + """ + Whether contributors are required to sign off on web-based commits for repositories in this organization. + """ + webCommitSignoffRequired: Boolean! + + """ + The organization's public profile URL. + """ + websiteUrl: URI +} + +""" +An audit entry in an organization audit log. +""" +union OrganizationAuditEntry = + MembersCanDeleteReposClearAuditEntry + | MembersCanDeleteReposDisableAuditEntry + | MembersCanDeleteReposEnableAuditEntry + | OauthApplicationCreateAuditEntry + | OrgAddBillingManagerAuditEntry + | OrgAddMemberAuditEntry + | OrgBlockUserAuditEntry + | OrgConfigDisableCollaboratorsOnlyAuditEntry + | OrgConfigEnableCollaboratorsOnlyAuditEntry + | OrgCreateAuditEntry + | OrgDisableOauthAppRestrictionsAuditEntry + | OrgDisableSamlAuditEntry + | OrgDisableTwoFactorRequirementAuditEntry + | OrgEnableOauthAppRestrictionsAuditEntry + | OrgEnableSamlAuditEntry + | OrgEnableTwoFactorRequirementAuditEntry + | OrgInviteMemberAuditEntry + | OrgInviteToBusinessAuditEntry + | OrgOauthAppAccessApprovedAuditEntry + | OrgOauthAppAccessBlockedAuditEntry + | OrgOauthAppAccessDeniedAuditEntry + | OrgOauthAppAccessRequestedAuditEntry + | OrgOauthAppAccessUnblockedAuditEntry + | OrgRemoveBillingManagerAuditEntry + | OrgRemoveMemberAuditEntry + | OrgRemoveOutsideCollaboratorAuditEntry + | OrgRestoreMemberAuditEntry + | OrgUnblockUserAuditEntry + | OrgUpdateDefaultRepositoryPermissionAuditEntry + | OrgUpdateMemberAuditEntry + | OrgUpdateMemberRepositoryCreationPermissionAuditEntry + | OrgUpdateMemberRepositoryInvitationPermissionAuditEntry + | PrivateRepositoryForkingDisableAuditEntry + | PrivateRepositoryForkingEnableAuditEntry + | RepoAccessAuditEntry + | RepoAddMemberAuditEntry + | RepoAddTopicAuditEntry + | RepoArchivedAuditEntry + | RepoChangeMergeSettingAuditEntry + | RepoConfigDisableAnonymousGitAccessAuditEntry + | RepoConfigDisableCollaboratorsOnlyAuditEntry + | RepoConfigDisableContributorsOnlyAuditEntry + | RepoConfigDisableSockpuppetDisallowedAuditEntry + | RepoConfigEnableAnonymousGitAccessAuditEntry + | RepoConfigEnableCollaboratorsOnlyAuditEntry + | RepoConfigEnableContributorsOnlyAuditEntry + | RepoConfigEnableSockpuppetDisallowedAuditEntry + | RepoConfigLockAnonymousGitAccessAuditEntry + | RepoConfigUnlockAnonymousGitAccessAuditEntry + | RepoCreateAuditEntry + | RepoDestroyAuditEntry + | RepoRemoveMemberAuditEntry + | RepoRemoveTopicAuditEntry + | RepositoryVisibilityChangeDisableAuditEntry + | RepositoryVisibilityChangeEnableAuditEntry + | TeamAddMemberAuditEntry + | TeamAddRepositoryAuditEntry + | TeamChangeParentTeamAuditEntry + | TeamRemoveMemberAuditEntry + | TeamRemoveRepositoryAuditEntry + +""" +The connection type for OrganizationAuditEntry. +""" +type OrganizationAuditEntryConnection { + """ + A list of edges. + """ + edges: [OrganizationAuditEntryEdge] + + """ + A list of nodes. + """ + nodes: [OrganizationAuditEntry] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Metadata for an audit entry with action org.* +""" +interface OrganizationAuditEntryData { + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI +} + +""" +An edge in a connection. +""" +type OrganizationAuditEntryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: OrganizationAuditEntry +} + +""" +A list of organizations managed by an enterprise. +""" +type OrganizationConnection { + """ + A list of edges. + """ + edges: [OrganizationEdge] + + """ + A list of nodes. + """ + nodes: [Organization] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type OrganizationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Organization +} + +""" +The connection type for User. +""" +type OrganizationEnterpriseOwnerConnection { + """ + A list of edges. + """ + edges: [OrganizationEnterpriseOwnerEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An enterprise owner in the context of an organization that is part of the enterprise. +""" +type OrganizationEnterpriseOwnerEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: User + + """ + The role of the owner with respect to the organization. + """ + organizationRole: RoleInOrganization! +} + +""" +An Identity Provider configured to provision SAML and SCIM identities for +Organizations. Visible to (1) organization owners, (2) organization owners' +personal access tokens (classic) with read:org or admin:org scope, (3) GitHub +App with an installation token with read or write access to members. +""" +type OrganizationIdentityProvider implements Node { + """ + The digest algorithm used to sign SAML requests for the Identity Provider. + """ + digestMethod: URI + + """ + External Identities provisioned by this Identity Provider + """ + externalIdentities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter to external identities with the users login + """ + login: String + + """ + Filter to external identities with valid org membership only + """ + membersOnly: Boolean + + """ + Filter to external identities with the users userName/NameID attribute + """ + userName: String + ): ExternalIdentityConnection! + id: ID! + + """ + The x509 certificate used by the Identity Provider to sign assertions and responses. + """ + idpCertificate: X509Certificate + + """ + The Issuer Entity ID for the SAML Identity Provider + """ + issuer: String + + """ + Organization this Identity Provider belongs to + """ + organization: Organization + + """ + The signature algorithm used to sign SAML requests for the Identity Provider. + """ + signatureMethod: URI + + """ + The URL endpoint for the Identity Provider's SAML SSO. + """ + ssoUrl: URI +} + +""" +An Invitation for a user to an organization. +""" +type OrganizationInvitation implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The email address of the user invited to the organization. + """ + email: String + id: ID! + + """ + The source of the invitation. + """ + invitationSource: OrganizationInvitationSource! + + """ + The type of invitation that was sent (e.g. email, user). + """ + invitationType: OrganizationInvitationType! + + """ + The user who was invited to the organization. + """ + invitee: User + + """ + The user who created the invitation. + """ + inviter: User! + + """ + The organization the invite is for + """ + organization: Organization! + + """ + The user's pending role in the organization (e.g. member, owner). + """ + role: OrganizationInvitationRole! +} + +""" +The connection type for OrganizationInvitation. +""" +type OrganizationInvitationConnection { + """ + A list of edges. + """ + edges: [OrganizationInvitationEdge] + + """ + A list of nodes. + """ + nodes: [OrganizationInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type OrganizationInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: OrganizationInvitation +} + +""" +The possible organization invitation roles. +""" +enum OrganizationInvitationRole { + """ + The user is invited to be an admin of the organization. + """ + ADMIN + + """ + The user is invited to be a billing manager of the organization. + """ + BILLING_MANAGER + + """ + The user is invited to be a direct member of the organization. + """ + DIRECT_MEMBER + + """ + The user's previous role will be reinstated. + """ + REINSTATE +} + +""" +The possible organization invitation sources. +""" +enum OrganizationInvitationSource { + """ + The invitation was created from the web interface or from API + """ + MEMBER + + """ + The invitation was created from SCIM + """ + SCIM + + """ + The invitation was sent before this feature was added + """ + UNKNOWN +} + +""" +The possible organization invitation types. +""" +enum OrganizationInvitationType { + """ + The invitation was to an email address. + """ + EMAIL + + """ + The invitation was to an existing user. + """ + USER +} + +""" +The connection type for User. +""" +type OrganizationMemberConnection { + """ + A list of edges. + """ + edges: [OrganizationMemberEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user within an organization. +""" +type OrganizationMemberEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + Whether the organization member has two factor enabled or not. Returns null if information is not available to viewer. + """ + hasTwoFactorEnabled: Boolean + + """ + The item at the end of the edge. + """ + node: User + + """ + The role this user has in the organization. + """ + role: OrganizationMemberRole +} + +""" +The possible roles within an organization for its members. +""" +enum OrganizationMemberRole { + """ + The user is an administrator of the organization. + """ + ADMIN + + """ + The user is a member of the organization. + """ + MEMBER +} + +""" +The possible values for the members can create repositories setting on an organization. +""" +enum OrganizationMembersCanCreateRepositoriesSettingValue { + """ + Members will be able to create public and private repositories. + """ + ALL + + """ + Members will not be able to create public or private repositories. + """ + DISABLED + + """ + Members will be able to create only internal repositories. + """ + INTERNAL + + """ + Members will be able to create only private repositories. + """ + PRIVATE +} + +""" +A GitHub Enterprise Importer (GEI) organization migration. +""" +type OrganizationMigration implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: String + + """ + The reason the organization migration failed. + """ + failureReason: String + id: ID! + + """ + The remaining amount of repos to be migrated. + """ + remainingRepositoriesCount: Int + + """ + The name of the source organization to be migrated. + """ + sourceOrgName: String! + + """ + The URL of the source organization to migrate. + """ + sourceOrgUrl: URI! + + """ + The migration state. + """ + state: OrganizationMigrationState! + + """ + The name of the target organization. + """ + targetOrgName: String! + + """ + The total amount of repositories to be migrated. + """ + totalRepositoriesCount: Int +} + +""" +The Octoshift Organization migration state. +""" +enum OrganizationMigrationState { + """ + The Octoshift migration has failed. + """ + FAILED + + """ + The Octoshift migration has invalid credentials. + """ + FAILED_VALIDATION + + """ + The Octoshift migration is in progress. + """ + IN_PROGRESS + + """ + The Octoshift migration has not started. + """ + NOT_STARTED + + """ + The Octoshift migration needs to have its credentials validated. + """ + PENDING_VALIDATION + + """ + The Octoshift migration is performing post repository migrations. + """ + POST_REPO_MIGRATION + + """ + The Octoshift migration is performing pre repository migrations. + """ + PRE_REPO_MIGRATION + + """ + The Octoshift migration has been queued. + """ + QUEUED + + """ + The Octoshift org migration is performing repository migrations. + """ + REPO_MIGRATION + + """ + The Octoshift migration has succeeded. + """ + SUCCEEDED +} + +""" +Used for argument of CreateProjectV2 mutation. +""" +union OrganizationOrUser = Organization | User + +""" +Ordering options for organization connections. +""" +input OrganizationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order organizations by. + """ + field: OrganizationOrderField! +} + +""" +Properties by which organization connections can be ordered. +""" +enum OrganizationOrderField { + """ + Order organizations by creation time + """ + CREATED_AT + + """ + Order organizations by login + """ + LOGIN +} + +""" +An organization teams hovercard context +""" +type OrganizationTeamsHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! + + """ + Teams in this organization the user is a member of that are relevant + """ + relevantTeams( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): TeamConnection! + + """ + The path for the full team list for this user + """ + teamsResourcePath: URI! + + """ + The URL for the full team list for this user + """ + teamsUrl: URI! + + """ + The total number of teams the user is on in the organization + """ + totalTeamCount: Int! +} + +""" +An organization list hovercard context +""" +type OrganizationsHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! + + """ + Organizations this user is a member of that are relevant + """ + relevantOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the User's organizations. + """ + orderBy: OrganizationOrder = null + ): OrganizationConnection! + + """ + The total number of organizations this user is in + """ + totalOrganizationCount: Int! +} + +""" +Information for an uploaded package. +""" +type Package implements Node { + id: ID! + + """ + Find the latest version for the package. + """ + latestVersion: PackageVersion + + """ + Identifies the name of the package. + """ + name: String! + + """ + Identifies the type of the package. + """ + packageType: PackageType! + + """ + The repository this package belongs to. + """ + repository: Repository + + """ + Statistics about package activity. + """ + statistics: PackageStatistics + + """ + Find package version by version string. + """ + version( + """ + The package version. + """ + version: String! + ): PackageVersion + + """ + list of versions for this package + """ + versions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering of the returned packages. + """ + orderBy: PackageVersionOrder = {field: CREATED_AT, direction: DESC} + ): PackageVersionConnection! +} + +""" +The connection type for Package. +""" +type PackageConnection { + """ + A list of edges. + """ + edges: [PackageEdge] + + """ + A list of nodes. + """ + nodes: [Package] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PackageEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Package +} + +""" +A file in a package version. +""" +type PackageFile implements Node { + id: ID! + + """ + MD5 hash of the file. + """ + md5: String + + """ + Name of the file. + """ + name: String! + + """ + The package version this file belongs to. + """ + packageVersion: PackageVersion + + """ + SHA1 hash of the file. + """ + sha1: String + + """ + SHA256 hash of the file. + """ + sha256: String + + """ + Size of the file in bytes. + """ + size: Int + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + URL to download the asset. + """ + url: URI +} + +""" +The connection type for PackageFile. +""" +type PackageFileConnection { + """ + A list of edges. + """ + edges: [PackageFileEdge] + + """ + A list of nodes. + """ + nodes: [PackageFile] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PackageFileEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PackageFile +} + +""" +Ways in which lists of package files can be ordered upon return. +""" +input PackageFileOrder { + """ + The direction in which to order package files by the specified field. + """ + direction: OrderDirection + + """ + The field in which to order package files by. + """ + field: PackageFileOrderField +} + +""" +Properties by which package file connections can be ordered. +""" +enum PackageFileOrderField { + """ + Order package files by creation time + """ + CREATED_AT +} + +""" +Ways in which lists of packages can be ordered upon return. +""" +input PackageOrder { + """ + The direction in which to order packages by the specified field. + """ + direction: OrderDirection + + """ + The field in which to order packages by. + """ + field: PackageOrderField +} + +""" +Properties by which package connections can be ordered. +""" +enum PackageOrderField { + """ + Order packages by creation time + """ + CREATED_AT +} + +""" +Represents an owner of a package. +""" +interface PackageOwner { + id: ID! + + """ + A list of packages under the owner. + """ + packages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Find packages by their names. + """ + names: [String] + + """ + Ordering of the returned packages. + """ + orderBy: PackageOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter registry package by type. + """ + packageType: PackageType + + """ + Find packages in a repository by ID. + """ + repositoryId: ID + ): PackageConnection! +} + +""" +Represents a object that contains package activity statistics such as downloads. +""" +type PackageStatistics { + """ + Number of times the package was downloaded since it was created. + """ + downloadsTotalCount: Int! +} + +""" +A version tag contains the mapping between a tag name and a version. +""" +type PackageTag implements Node { + id: ID! + + """ + Identifies the tag name of the version. + """ + name: String! + + """ + Version that the tag is associated with. + """ + version: PackageVersion +} + +""" +The possible types of a package. +""" +enum PackageType { + """ + A debian package. + """ + DEBIAN + + """ + A docker image. + """ + DOCKER + @deprecated( + reason: "DOCKER will be removed from this enum as this type will be migrated to only be used by the Packages REST API. Removal on 2021-06-21 UTC." + ) + + """ + A maven package. + """ + MAVEN + @deprecated( + reason: "MAVEN will be removed from this enum as this type will be migrated to only be used by the Packages REST API. Removal on 2023-02-10 UTC." + ) + + """ + An npm package. + """ + NPM + @deprecated( + reason: "NPM will be removed from this enum as this type will be migrated to only be used by the Packages REST API. Removal on 2022-11-21 UTC." + ) + + """ + A nuget package. + """ + NUGET + @deprecated( + reason: "NUGET will be removed from this enum as this type will be migrated to only be used by the Packages REST API. Removal on 2022-11-21 UTC." + ) + + """ + A python package. + """ + PYPI + + """ + A rubygems package. + """ + RUBYGEMS + @deprecated( + reason: "RUBYGEMS will be removed from this enum as this type will be migrated to only be used by the Packages REST API. Removal on 2022-12-28 UTC." + ) +} + +""" +Information about a specific package version. +""" +type PackageVersion implements Node { + """ + List of files associated with this package version + """ + files( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering of the returned package files. + """ + orderBy: PackageFileOrder = {field: CREATED_AT, direction: ASC} + ): PackageFileConnection! + id: ID! + + """ + The package associated with this version. + """ + package: Package + + """ + The platform this version was built for. + """ + platform: String + + """ + Whether or not this version is a pre-release. + """ + preRelease: Boolean! + + """ + The README of this package version. + """ + readme: String + + """ + The release associated with this package version. + """ + release: Release + + """ + Statistics about package activity. + """ + statistics: PackageVersionStatistics + + """ + The package version summary. + """ + summary: String + + """ + The version string. + """ + version: String! +} + +""" +The connection type for PackageVersion. +""" +type PackageVersionConnection { + """ + A list of edges. + """ + edges: [PackageVersionEdge] + + """ + A list of nodes. + """ + nodes: [PackageVersion] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PackageVersionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PackageVersion +} + +""" +Ways in which lists of package versions can be ordered upon return. +""" +input PackageVersionOrder { + """ + The direction in which to order package versions by the specified field. + """ + direction: OrderDirection + + """ + The field in which to order package versions by. + """ + field: PackageVersionOrderField +} + +""" +Properties by which package version connections can be ordered. +""" +enum PackageVersionOrderField { + """ + Order package versions by creation time + """ + CREATED_AT +} + +""" +Represents a object that contains package version activity statistics such as downloads. +""" +type PackageVersionStatistics { + """ + Number of times the package was downloaded since it was created. + """ + downloadsTotalCount: Int! +} + +""" +Information about pagination in a connection. +""" +type PageInfo { + """ + When paginating forwards, the cursor to continue. + """ + endCursor: String + + """ + When paginating forwards, are there more items? + """ + hasNextPage: Boolean! + + """ + When paginating backwards, are there more items? + """ + hasPreviousPage: Boolean! + + """ + When paginating backwards, the cursor to continue. + """ + startCursor: String +} + +""" +The possible types of patch statuses. +""" +enum PatchStatus { + """ + The file was added. Git status 'A'. + """ + ADDED + + """ + The file's type was changed. Git status 'T'. + """ + CHANGED + + """ + The file was copied. Git status 'C'. + """ + COPIED + + """ + The file was deleted. Git status 'D'. + """ + DELETED + + """ + The file's contents were changed. Git status 'M'. + """ + MODIFIED + + """ + The file was renamed. Git status 'R'. + """ + RENAMED +} + +""" +Types that can grant permissions on a repository to a user +""" +union PermissionGranter = Organization | Repository | Team + +""" +A level of permission and source for a user's access to a repository. +""" +type PermissionSource { + """ + The organization the repository belongs to. + """ + organization: Organization! + + """ + The level of access this source has granted to the user. + """ + permission: DefaultRepositoryPermissionField! + + """ + The source of this permission. + """ + source: PermissionGranter! +} + +""" +Autogenerated input type of PinIssue +""" +input PinIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the issue to be pinned + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of PinIssue +""" +type PinIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was pinned + """ + issue: Issue +} + +""" +Types that can be pinned to a profile page. +""" +union PinnableItem = Gist | Repository + +""" +The connection type for PinnableItem. +""" +type PinnableItemConnection { + """ + A list of edges. + """ + edges: [PinnableItemEdge] + + """ + A list of nodes. + """ + nodes: [PinnableItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PinnableItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PinnableItem +} + +""" +Represents items that can be pinned to a profile page or dashboard. +""" +enum PinnableItemType { + """ + A gist. + """ + GIST + + """ + An issue. + """ + ISSUE + + """ + An organization. + """ + ORGANIZATION + + """ + A project. + """ + PROJECT + + """ + A pull request. + """ + PULL_REQUEST + + """ + A repository. + """ + REPOSITORY + + """ + A team. + """ + TEAM + + """ + A user. + """ + USER +} + +""" +A Pinned Discussion is a discussion pinned to a repository's index page. +""" +type PinnedDiscussion implements Node & RepositoryNode { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The discussion that was pinned. + """ + discussion: Discussion! + + """ + Color stops of the chosen gradient + """ + gradientStopColors: [String!]! + id: ID! + + """ + Background texture pattern + """ + pattern: PinnedDiscussionPattern! + + """ + The actor that pinned this discussion. + """ + pinnedBy: Actor! + + """ + Preconfigured background gradient option + """ + preconfiguredGradient: PinnedDiscussionGradient + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for PinnedDiscussion. +""" +type PinnedDiscussionConnection { + """ + A list of edges. + """ + edges: [PinnedDiscussionEdge] + + """ + A list of nodes. + """ + nodes: [PinnedDiscussion] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PinnedDiscussionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PinnedDiscussion +} + +""" +Preconfigured gradients that may be used to style discussions pinned within a repository. +""" +enum PinnedDiscussionGradient { + """ + A gradient of blue to mint + """ + BLUE_MINT + + """ + A gradient of blue to purple + """ + BLUE_PURPLE + + """ + A gradient of pink to blue + """ + PINK_BLUE + + """ + A gradient of purple to coral + """ + PURPLE_CORAL + + """ + A gradient of red to orange + """ + RED_ORANGE +} + +""" +Preconfigured background patterns that may be used to style discussions pinned within a repository. +""" +enum PinnedDiscussionPattern { + """ + An upward-facing chevron pattern + """ + CHEVRON_UP + + """ + A hollow dot pattern + """ + DOT + + """ + A solid dot pattern + """ + DOT_FILL + + """ + A heart pattern + """ + HEART_FILL + + """ + A plus sign pattern + """ + PLUS + + """ + A lightning bolt pattern + """ + ZAP +} + +""" +Represents a 'pinned' event on a given issue or pull request. +""" +type PinnedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the issue associated with the event. + """ + issue: Issue! +} + +""" +A Pinned Issue is a issue pinned to a repository's index page. +""" +type PinnedIssue implements Node { + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + id: ID! + + """ + The issue that was pinned. + """ + issue: Issue! + + """ + The actor that pinned this issue. + """ + pinnedBy: Actor! + + """ + The repository that this issue was pinned to. + """ + repository: Repository! +} + +""" +The connection type for PinnedIssue. +""" +type PinnedIssueConnection { + """ + A list of edges. + """ + edges: [PinnedIssueEdge] + + """ + A list of nodes. + """ + nodes: [PinnedIssue] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PinnedIssueEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PinnedIssue +} + +""" +An ISO-8601 encoded UTC date string with millisecond precision. +""" +scalar PreciseDateTime + +""" +Audit log entry for a private_repository_forking.disable event. +""" +type PrivateRepositoryForkingDisableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a private_repository_forking.enable event. +""" +type PrivateRepositoryForkingEnableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +A curatable list of repositories relating to a repository owner, which defaults +to showing the most popular repositories they own. +""" +type ProfileItemShowcase { + """ + Whether or not the owner has pinned any repositories or gists. + """ + hasPinnedItems: Boolean! + + """ + The repositories and gists in the showcase. If the profile owner has any + pinned items, those will be returned. Otherwise, the profile owner's popular + repositories will be returned. + """ + items( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PinnableItemConnection! +} + +""" +Represents any entity on GitHub that has a profile page. +""" +interface ProfileOwner { + """ + Determine if this repository owner has any items that can be pinned to their profile. + """ + anyPinnableItems( + """ + Filter to only a particular kind of pinnable item. + """ + type: PinnableItemType + ): Boolean! + + """ + The public profile email. + """ + email: String + id: ID! + + """ + Showcases a selection of repositories and gists that the profile owner has + either curated or that have been selected automatically based on popularity. + """ + itemShowcase: ProfileItemShowcase! + + """ + The public profile location. + """ + location: String + + """ + The username used to login. + """ + login: String! + + """ + The public profile name. + """ + name: String + + """ + A list of repositories and gists this profile owner can pin to their profile. + """ + pinnableItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinnable items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + A list of repositories and gists this profile owner has pinned to their profile + """ + pinnedItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinned items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + Returns how many more items this profile owner can pin to their profile. + """ + pinnedItemsRemaining: Int! + + """ + Can the viewer pin repositories and gists to the profile? + """ + viewerCanChangePinnedItems: Boolean! + + """ + The public profile website URL. + """ + websiteUrl: URI +} + +""" +Projects manage issues, pull requests and notes within a project owner. +""" +type Project implements Closable & Node & Updatable { + """ + The project's description body. + """ + body: String + + """ + The projects description body rendered to HTML. + """ + bodyHTML: HTML! + + """ + Indicates if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + List of columns in the project + """ + columns( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectColumnConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who originally created the project. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The project's name. + """ + name: String! + + """ + The project's number. + """ + number: Int! + + """ + The project's owner. Currently limited to repositories, organizations, and users. + """ + owner: ProjectOwner! + + """ + List of pending cards in this project + """ + pendingCards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + A list of archived states to filter the cards by + """ + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectCardConnection! + + """ + Project progress details. + """ + progress: ProjectProgress! + + """ + The HTTP path for this project + """ + resourcePath: URI! + + """ + Whether the project is open or closed. + """ + state: ProjectState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this project + """ + url: URI! + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! +} + +""" +A card in a project. +""" +type ProjectCard implements Node { + """ + The project column this card is associated under. A card may only belong to one + project column at a time. The column field will be null if the card is created + in a pending state and has yet to be associated with a column. Once cards are + associated with a column, they will not become pending in the future. + """ + column: ProjectColumn + + """ + The card content item + """ + content: ProjectCardItem + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created this card + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Whether the card is archived + """ + isArchived: Boolean! + + """ + The card note + """ + note: String + + """ + The project that contains this card. + """ + project: Project! + + """ + The HTTP path for this card + """ + resourcePath: URI! + + """ + The state of ProjectCard + """ + state: ProjectCardState + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this card + """ + url: URI! +} + +""" +The possible archived states of a project card. +""" +enum ProjectCardArchivedState { + """ + A project card that is archived + """ + ARCHIVED + + """ + A project card that is not archived + """ + NOT_ARCHIVED +} + +""" +The connection type for ProjectCard. +""" +type ProjectCardConnection { + """ + A list of edges. + """ + edges: [ProjectCardEdge] + + """ + A list of nodes. + """ + nodes: [ProjectCard] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectCardEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectCard +} + +""" +An issue or PR and its owning repository to be used in a project card. +""" +input ProjectCardImport { + """ + The issue or pull request number. + """ + number: Int! + + """ + Repository name with owner (owner/repository). + """ + repository: String! +} + +""" +Types that can be inside Project Cards. +""" +union ProjectCardItem = Issue | PullRequest + +""" +Various content states of a ProjectCard +""" +enum ProjectCardState { + """ + The card has content only. + """ + CONTENT_ONLY + + """ + The card has a note only. + """ + NOTE_ONLY + + """ + The card is redacted. + """ + REDACTED +} + +""" +A column inside a project. +""" +type ProjectColumn implements Node { + """ + List of cards in the column + """ + cards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + A list of archived states to filter the cards by + """ + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectCardConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The project column's name. + """ + name: String! + + """ + The project that contains this column. + """ + project: Project! + + """ + The semantic purpose of the column + """ + purpose: ProjectColumnPurpose + + """ + The HTTP path for this project column + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this project column + """ + url: URI! +} + +""" +The connection type for ProjectColumn. +""" +type ProjectColumnConnection { + """ + A list of edges. + """ + edges: [ProjectColumnEdge] + + """ + A list of nodes. + """ + nodes: [ProjectColumn] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectColumnEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectColumn +} + +""" +A project column and a list of its issues and PRs. +""" +input ProjectColumnImport { + """ + The name of the column. + """ + columnName: String! + + """ + A list of issues and pull requests in the column. + """ + issues: [ProjectCardImport!] + + """ + The position of the column, starting from 0. + """ + position: Int! +} + +""" +The semantic purpose of the column - todo, in progress, or done. +""" +enum ProjectColumnPurpose { + """ + The column contains cards which are complete + """ + DONE + + """ + The column contains cards which are currently being worked on + """ + IN_PROGRESS + + """ + The column contains cards still to be worked on + """ + TODO +} + +""" +A list of projects associated with the owner. +""" +type ProjectConnection { + """ + A list of edges. + """ + edges: [ProjectEdge] + + """ + A list of nodes. + """ + nodes: [Project] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Project +} + +""" +Ways in which lists of projects can be ordered upon return. +""" +input ProjectOrder { + """ + The direction in which to order projects by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order projects by. + """ + field: ProjectOrderField! +} + +""" +Properties by which project connections can be ordered. +""" +enum ProjectOrderField { + """ + Order projects by creation time + """ + CREATED_AT + + """ + Order projects by name + """ + NAME + + """ + Order projects by update time + """ + UPDATED_AT +} + +""" +Represents an owner of a Project. +""" +interface ProjectOwner { + id: ID! + + """ + Find project by number. + """ + project( + """ + The project number to find. + """ + number: Int! + ): Project + + """ + A list of projects under the owner. + """ + projects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for projects returned from the connection + """ + orderBy: ProjectOrder + + """ + Query to search projects by, currently only searching by name. + """ + search: String + + """ + A list of states to filter the projects by. + """ + states: [ProjectState!] + ): ProjectConnection! + + """ + The HTTP path listing owners projects + """ + projectsResourcePath: URI! + + """ + The HTTP URL listing owners projects + """ + projectsUrl: URI! + + """ + Can the current viewer create new projects on this owner. + """ + viewerCanCreateProjects: Boolean! +} + +""" +Project progress stats. +""" +type ProjectProgress { + """ + The number of done cards. + """ + doneCount: Int! + + """ + The percentage of done cards. + """ + donePercentage: Float! + + """ + Whether progress tracking is enabled and cards with purpose exist for this project + """ + enabled: Boolean! + + """ + The number of in-progress cards. + """ + inProgressCount: Int! + + """ + The percentage of in-progress cards. + """ + inProgressPercentage: Float! + + """ + The number of to do cards. + """ + todoCount: Int! + + """ + The percentage of to do cards. + """ + todoPercentage: Float! +} + +""" +State of the project; either 'open' or 'closed' +""" +enum ProjectState { + """ + The project is closed. + """ + CLOSED + + """ + The project is open. + """ + OPEN +} + +""" +GitHub-provided templates for Projects +""" +enum ProjectTemplate { + """ + Create a board with v2 triggers to automatically move cards across To do, In progress and Done columns. + """ + AUTOMATED_KANBAN_V2 + + """ + Create a board with triggers to automatically move cards across columns with review automation. + """ + AUTOMATED_REVIEWS_KANBAN + + """ + Create a board with columns for To do, In progress and Done. + """ + BASIC_KANBAN + + """ + Create a board to triage and prioritize bugs with To do, priority, and Done columns. + """ + BUG_TRIAGE +} + +""" +New projects that manage issues, pull requests and drafts using tables and boards. +""" +type ProjectV2 implements Closable & Node & Updatable { + """ + Returns true if the project is closed. + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who originally created the project. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + A field of the project + """ + field( + """ + The name of the field + """ + name: String! + ): ProjectV2FieldConfiguration + + """ + List of fields and their constraints in the project + """ + fields( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for project v2 fields returned from the connection + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConfigurationConnection! + id: ID! + + """ + List of items in the project + """ + items( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for project v2 items returned from the connection + """ + orderBy: ProjectV2ItemOrder = {field: POSITION, direction: ASC} + ): ProjectV2ItemConnection! + + """ + The project's number. + """ + number: Int! + + """ + The project's owner. Currently limited to organizations and users. + """ + owner: ProjectV2Owner! + + """ + Returns true if the project is public. + """ + public: Boolean! + + """ + The project's readme. + """ + readme: String + + """ + The repositories the project is linked to. + """ + repositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder = {field: CREATED_AT, direction: DESC} + ): RepositoryConnection! + + """ + The HTTP path for this project + """ + resourcePath: URI! + + """ + The project's short description. + """ + shortDescription: String + + """ + The teams the project is linked to. + """ + teams( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for teams returned from this connection. + """ + orderBy: TeamOrder = {field: NAME, direction: ASC} + ): TeamConnection! + + """ + Returns true if this project is a template. + """ + template: Boolean! + + """ + The project's name. + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this project + """ + url: URI! + + """ + A view of the project + """ + view( + """ + The number of a view belonging to the project + """ + number: Int! + ): ProjectV2View + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + List of views in the project + """ + views( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for project v2 views returned from the connection + """ + orderBy: ProjectV2ViewOrder = {field: POSITION, direction: ASC} + ): ProjectV2ViewConnection! + + """ + A workflow of the project + """ + workflow( + """ + The number of a workflow belonging to the project + """ + number: Int! + ): ProjectV2Workflow + + """ + List of the workflows in the project + """ + workflows( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for project v2 workflows returned from the connection + """ + orderBy: ProjectV2WorkflowOrder = {field: NAME, direction: ASC} + ): ProjectV2WorkflowConnection! +} + +""" +Possible collaborators for a project. +""" +union ProjectV2Actor = Team | User + +""" +The connection type for ProjectV2Actor. +""" +type ProjectV2ActorConnection { + """ + A list of edges. + """ + edges: [ProjectV2ActorEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2Actor] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2ActorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2Actor +} + +""" +A collaborator to update on a project. Only one of the userId or teamId should be provided. +""" +input ProjectV2Collaborator { + """ + The role to grant the collaborator + """ + role: ProjectV2Roles! + + """ + The ID of the team as a collaborator. + """ + teamId: ID @possibleTypes(concreteTypes: ["Team"]) + + """ + The ID of the user as a collaborator. + """ + userId: ID @possibleTypes(concreteTypes: ["User"]) +} + +""" +The connection type for ProjectV2. +""" +type ProjectV2Connection { + """ + A list of edges. + """ + edges: [ProjectV2Edge] + + """ + A list of nodes. + """ + nodes: [ProjectV2] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The type of a project field. +""" +enum ProjectV2CustomFieldType { + """ + Date + """ + DATE + + """ + Number + """ + NUMBER + + """ + Single Select + """ + SINGLE_SELECT + + """ + Text + """ + TEXT +} + +""" +An edge in a connection. +""" +type ProjectV2Edge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2 +} + +""" +A field inside a project. +""" +type ProjectV2Field implements Node & ProjectV2FieldCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The field's type. + """ + dataType: ProjectV2FieldType! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The project field's name. + """ + name: String! + + """ + The project that contains this field. + """ + project: ProjectV2! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +Common fields across different project field types +""" +interface ProjectV2FieldCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The field's type. + """ + dataType: ProjectV2FieldType! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The project field's name. + """ + name: String! + + """ + The project that contains this field. + """ + project: ProjectV2! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +Configurations for project fields. +""" +union ProjectV2FieldConfiguration = ProjectV2Field | ProjectV2IterationField | ProjectV2SingleSelectField + +""" +The connection type for ProjectV2FieldConfiguration. +""" +type ProjectV2FieldConfigurationConnection { + """ + A list of edges. + """ + edges: [ProjectV2FieldConfigurationEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2FieldConfiguration] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2FieldConfigurationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2FieldConfiguration +} + +""" +The connection type for ProjectV2Field. +""" +type ProjectV2FieldConnection { + """ + A list of edges. + """ + edges: [ProjectV2FieldEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2Field] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2FieldEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2Field +} + +""" +Ordering options for project v2 field connections +""" +input ProjectV2FieldOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order the project v2 fields by. + """ + field: ProjectV2FieldOrderField! +} + +""" +Properties by which project v2 field connections can be ordered. +""" +enum ProjectV2FieldOrderField { + """ + Order project v2 fields by creation time + """ + CREATED_AT + + """ + Order project v2 fields by name + """ + NAME + + """ + Order project v2 fields by position + """ + POSITION +} + +""" +The type of a project field. +""" +enum ProjectV2FieldType { + """ + Assignees + """ + ASSIGNEES + + """ + Date + """ + DATE + + """ + Iteration + """ + ITERATION + + """ + Labels + """ + LABELS + + """ + Linked Pull Requests + """ + LINKED_PULL_REQUESTS + + """ + Milestone + """ + MILESTONE + + """ + Number + """ + NUMBER + + """ + Repository + """ + REPOSITORY + + """ + Reviewers + """ + REVIEWERS + + """ + Single Select + """ + SINGLE_SELECT + + """ + Text + """ + TEXT + + """ + Title + """ + TITLE + + """ + Tracked by + """ + TRACKED_BY + + """ + Tracks + """ + TRACKS +} + +""" +The values that can be used to update a field of an item inside a Project. Only 1 value can be updated at a time. +""" +input ProjectV2FieldValue { + """ + The ISO 8601 date to set on the field. + """ + date: Date + + """ + The id of the iteration to set on the field. + """ + iterationId: String + + """ + The number to set on the field. + """ + number: Float + + """ + The id of the single select option to set on the field. + """ + singleSelectOptionId: String + + """ + The text to set on the field. + """ + text: String +} + +""" +Ways in which to filter lists of projects. +""" +input ProjectV2Filters { + """ + List project v2 filtered by the state given. + """ + state: ProjectV2State +} + +""" +An item within a Project. +""" +type ProjectV2Item implements Node { + """ + The content of the referenced draft issue, issue, or pull request + """ + content: ProjectV2ItemContent + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The field value of the first project field which matches the 'name' argument that is set on the item. + """ + fieldValueByName( + """ + The name of the field to return the field value of + """ + name: String! + ): ProjectV2ItemFieldValue + + """ + The field values that are set on the item. + """ + fieldValues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for project v2 item field values returned from the connection + """ + orderBy: ProjectV2ItemFieldValueOrder = {field: POSITION, direction: ASC} + ): ProjectV2ItemFieldValueConnection! + id: ID! + + """ + Whether the item is archived. + """ + isArchived: Boolean! + + """ + The project that contains this item. + """ + project: ProjectV2! + + """ + The type of the item. + """ + type: ProjectV2ItemType! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for ProjectV2Item. +""" +type ProjectV2ItemConnection { + """ + A list of edges. + """ + edges: [ProjectV2ItemEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2Item] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Types that can be inside Project Items. +""" +union ProjectV2ItemContent = DraftIssue | Issue | PullRequest + +""" +An edge in a connection. +""" +type ProjectV2ItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2Item +} + +""" +The value of a date field in a Project item. +""" +type ProjectV2ItemFieldDateValue implements Node & ProjectV2ItemFieldValueCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + Date value for the field + """ + date: Date + + """ + The project field that contains this value. + """ + field: ProjectV2FieldConfiguration! + id: ID! + + """ + The project item that contains this value. + """ + item: ProjectV2Item! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The value of an iteration field in a Project item. +""" +type ProjectV2ItemFieldIterationValue implements Node & ProjectV2ItemFieldValueCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The duration of the iteration in days. + """ + duration: Int! + + """ + The project field that contains this value. + """ + field: ProjectV2FieldConfiguration! + id: ID! + + """ + The project item that contains this value. + """ + item: ProjectV2Item! + + """ + The ID of the iteration. + """ + iterationId: String! + + """ + The start date of the iteration. + """ + startDate: Date! + + """ + The title of the iteration. + """ + title: String! + + """ + The title of the iteration, with HTML. + """ + titleHTML: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The value of the labels field in a Project item. +""" +type ProjectV2ItemFieldLabelValue { + """ + The field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + Labels value of a field + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): LabelConnection +} + +""" +The value of a milestone field in a Project item. +""" +type ProjectV2ItemFieldMilestoneValue { + """ + The field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + Milestone value of a field + """ + milestone: Milestone +} + +""" +The value of a number field in a Project item. +""" +type ProjectV2ItemFieldNumberValue implements Node & ProjectV2ItemFieldValueCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The project field that contains this value. + """ + field: ProjectV2FieldConfiguration! + id: ID! + + """ + The project item that contains this value. + """ + item: ProjectV2Item! + + """ + Number as a float(8) + """ + number: Float + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The value of a pull request field in a Project item. +""" +type ProjectV2ItemFieldPullRequestValue { + """ + The field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The pull requests for this field + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests. + """ + orderBy: PullRequestOrder = {field: CREATED_AT, direction: ASC} + ): PullRequestConnection +} + +""" +The value of a repository field in a Project item. +""" +type ProjectV2ItemFieldRepositoryValue { + """ + The field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The repository for this field. + """ + repository: Repository +} + +""" +The value of a reviewers field in a Project item. +""" +type ProjectV2ItemFieldReviewerValue { + """ + The field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The reviewers for this field. + """ + reviewers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RequestedReviewerConnection +} + +""" +The value of a single select field in a Project item. +""" +type ProjectV2ItemFieldSingleSelectValue implements Node & ProjectV2ItemFieldValueCommon { + """ + The color applied to the selected single-select option. + """ + color: ProjectV2SingleSelectFieldOptionColor! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + A plain-text description of the selected single-select option, such as what the option means. + """ + description: String + + """ + The description of the selected single-select option, including HTML tags. + """ + descriptionHTML: String + + """ + The project field that contains this value. + """ + field: ProjectV2FieldConfiguration! + id: ID! + + """ + The project item that contains this value. + """ + item: ProjectV2Item! + + """ + The name of the selected single select option. + """ + name: String + + """ + The html name of the selected single select option. + """ + nameHTML: String + + """ + The id of the selected single select option. + """ + optionId: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The value of a text field in a Project item. +""" +type ProjectV2ItemFieldTextValue implements Node & ProjectV2ItemFieldValueCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The project field that contains this value. + """ + field: ProjectV2FieldConfiguration! + id: ID! + + """ + The project item that contains this value. + """ + item: ProjectV2Item! + + """ + Text value of a field + """ + text: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The value of a user field in a Project item. +""" +type ProjectV2ItemFieldUserValue { + """ + The field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The users for this field + """ + users( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection +} + +""" +Project field values +""" +union ProjectV2ItemFieldValue = + ProjectV2ItemFieldDateValue + | ProjectV2ItemFieldIterationValue + | ProjectV2ItemFieldLabelValue + | ProjectV2ItemFieldMilestoneValue + | ProjectV2ItemFieldNumberValue + | ProjectV2ItemFieldPullRequestValue + | ProjectV2ItemFieldRepositoryValue + | ProjectV2ItemFieldReviewerValue + | ProjectV2ItemFieldSingleSelectValue + | ProjectV2ItemFieldTextValue + | ProjectV2ItemFieldUserValue + +""" +Common fields across different project field value types +""" +interface ProjectV2ItemFieldValueCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The project field that contains this value. + """ + field: ProjectV2FieldConfiguration! + id: ID! + + """ + The project item that contains this value. + """ + item: ProjectV2Item! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for ProjectV2ItemFieldValue. +""" +type ProjectV2ItemFieldValueConnection { + """ + A list of edges. + """ + edges: [ProjectV2ItemFieldValueEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2ItemFieldValue] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2ItemFieldValueEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2ItemFieldValue +} + +""" +Ordering options for project v2 item field value connections +""" +input ProjectV2ItemFieldValueOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order the project v2 item field values by. + """ + field: ProjectV2ItemFieldValueOrderField! +} + +""" +Properties by which project v2 item field value connections can be ordered. +""" +enum ProjectV2ItemFieldValueOrderField { + """ + Order project v2 item field values by the their position in the project + """ + POSITION +} + +""" +Ordering options for project v2 item connections +""" +input ProjectV2ItemOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order the project v2 items by. + """ + field: ProjectV2ItemOrderField! +} + +""" +Properties by which project v2 item connections can be ordered. +""" +enum ProjectV2ItemOrderField { + """ + Order project v2 items by the their position in the project + """ + POSITION +} + +""" +The type of a project item. +""" +enum ProjectV2ItemType { + """ + Draft Issue + """ + DRAFT_ISSUE + + """ + Issue + """ + ISSUE + + """ + Pull Request + """ + PULL_REQUEST + + """ + Redacted Item + """ + REDACTED +} + +""" +An iteration field inside a project. +""" +type ProjectV2IterationField implements Node & ProjectV2FieldCommon { + """ + Iteration configuration settings + """ + configuration: ProjectV2IterationFieldConfiguration! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The field's type. + """ + dataType: ProjectV2FieldType! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The project field's name. + """ + name: String! + + """ + The project that contains this field. + """ + project: ProjectV2! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +Iteration field configuration for a project. +""" +type ProjectV2IterationFieldConfiguration { + """ + The iteration's completed iterations + """ + completedIterations: [ProjectV2IterationFieldIteration!]! + + """ + The iteration's duration in days + """ + duration: Int! + + """ + The iteration's iterations + """ + iterations: [ProjectV2IterationFieldIteration!]! + + """ + The iteration's start day of the week + """ + startDay: Int! +} + +""" +Iteration field iteration settings for a project. +""" +type ProjectV2IterationFieldIteration { + """ + The iteration's duration in days + """ + duration: Int! + + """ + The iteration's ID. + """ + id: String! + + """ + The iteration's start date + """ + startDate: Date! + + """ + The iteration's title. + """ + title: String! + + """ + The iteration's html title. + """ + titleHTML: String! +} + +""" +Ways in which lists of projects can be ordered upon return. +""" +input ProjectV2Order { + """ + The direction in which to order projects by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order projects by. + """ + field: ProjectV2OrderField! +} + +""" +Properties by which projects can be ordered. +""" +enum ProjectV2OrderField { + """ + The project's date and time of creation + """ + CREATED_AT + + """ + The project's number + """ + NUMBER + + """ + The project's title + """ + TITLE + + """ + The project's date and time of update + """ + UPDATED_AT +} + +""" +Represents an owner of a project (beta). +""" +interface ProjectV2Owner { + id: ID! + + """ + Find a project by number. + """ + projectV2( + """ + The project number. + """ + number: Int! + ): ProjectV2 + + """ + A list of projects under the owner. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + A project to search for under the the owner. + """ + query: String + ): ProjectV2Connection! +} + +""" +Recent projects for the owner. +""" +interface ProjectV2Recent { + """ + Recent projects that this user has modified in the context of the owner. + """ + recentProjects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2Connection! +} + +""" +The possible roles of a collaborator on a project. +""" +enum ProjectV2Roles { + """ + The collaborator can view, edit, and maange the settings of the project + """ + ADMIN + + """ + The collaborator has no direct access to the project + """ + NONE + + """ + The collaborator can view the project + """ + READER + + """ + The collaborator can view and edit the project + """ + WRITER +} + +""" +A single select field inside a project. +""" +type ProjectV2SingleSelectField implements Node & ProjectV2FieldCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The field's type. + """ + dataType: ProjectV2FieldType! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The project field's name. + """ + name: String! + + """ + Options for the single select field + """ + options( + """ + Filter returned options to only those matching these names, case insensitive. + """ + names: [String!] + ): [ProjectV2SingleSelectFieldOption!]! + + """ + The project that contains this field. + """ + project: ProjectV2! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +Single select field option for a configuration for a project. +""" +type ProjectV2SingleSelectFieldOption { + """ + The option's display color. + """ + color: ProjectV2SingleSelectFieldOptionColor! + + """ + The option's plain-text description. + """ + description: String! + + """ + The option's description, possibly containing HTML. + """ + descriptionHTML: String! + + """ + The option's ID. + """ + id: String! + + """ + The option's name. + """ + name: String! + + """ + The option's html name. + """ + nameHTML: String! +} + +""" +The display color of a single-select field option. +""" +enum ProjectV2SingleSelectFieldOptionColor { + """ + BLUE + """ + BLUE + + """ + GRAY + """ + GRAY + + """ + GREEN + """ + GREEN + + """ + ORANGE + """ + ORANGE + + """ + PINK + """ + PINK + + """ + PURPLE + """ + PURPLE + + """ + RED + """ + RED + + """ + YELLOW + """ + YELLOW +} + +""" +Represents a single select field option +""" +input ProjectV2SingleSelectFieldOptionInput { + """ + The display color of the option + """ + color: ProjectV2SingleSelectFieldOptionColor! + + """ + The description text of the option + """ + description: String! + + """ + The name of the option + """ + name: String! +} + +""" +Represents a sort by field and direction. +""" +type ProjectV2SortBy { + """ + The direction of the sorting. Possible values are ASC and DESC. + """ + direction: OrderDirection! + + """ + The field by which items are sorted. + """ + field: ProjectV2Field! +} + +""" +The connection type for ProjectV2SortBy. +""" +type ProjectV2SortByConnection { + """ + A list of edges. + """ + edges: [ProjectV2SortByEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2SortBy] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2SortByEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2SortBy +} + +""" +Represents a sort by field and direction. +""" +type ProjectV2SortByField { + """ + The direction of the sorting. Possible values are ASC and DESC. + """ + direction: OrderDirection! + + """ + The field by which items are sorted. + """ + field: ProjectV2FieldConfiguration! +} + +""" +The connection type for ProjectV2SortByField. +""" +type ProjectV2SortByFieldConnection { + """ + A list of edges. + """ + edges: [ProjectV2SortByFieldEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2SortByField] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2SortByFieldEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2SortByField +} + +""" +The possible states of a project v2. +""" +enum ProjectV2State { + """ + A project v2 that has been closed + """ + CLOSED + + """ + A project v2 that is still open + """ + OPEN +} + +""" +A view within a ProjectV2. +""" +type ProjectV2View implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The view's visible fields. + """ + fields( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the project v2 fields returned from the connection. + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConfigurationConnection + + """ + The project view's filter. + """ + filter: String + + """ + The view's group-by field. + """ + groupBy( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the project v2 fields returned from the connection. + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConnection + @deprecated( + reason: "The `ProjectV2View#order_by` API is deprecated in favour of the more capable `ProjectV2View#group_by_field` API. Check out the `ProjectV2View#group_by_fields` API as an example for the more capable alternative. Removal on 2023-04-01 UTC." + ) + + """ + The view's group-by field. + """ + groupByFields( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the project v2 fields returned from the connection. + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConfigurationConnection + id: ID! + + """ + The project view's layout. + """ + layout: ProjectV2ViewLayout! + + """ + The project view's name. + """ + name: String! + + """ + The project view's number. + """ + number: Int! + + """ + The project that contains this view. + """ + project: ProjectV2! + + """ + The view's sort-by config. + """ + sortBy( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2SortByConnection + @deprecated( + reason: "The `ProjectV2View#sort_by` API is deprecated in favour of the more capable `ProjectV2View#sort_by_fields` API. Check out the `ProjectV2View#sort_by_fields` API as an example for the more capable alternative. Removal on 2023-04-01 UTC." + ) + + """ + The view's sort-by config. + """ + sortByFields( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2SortByFieldConnection + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The view's vertical-group-by field. + """ + verticalGroupBy( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the project v2 fields returned from the connection. + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConnection + @deprecated( + reason: "The `ProjectV2View#vertical_group_by` API is deprecated in favour of the more capable `ProjectV2View#vertical_group_by_fields` API. Check out the `ProjectV2View#vertical_group_by_fields` API as an example for the more capable alternative. Removal on 2023-04-01 UTC." + ) + + """ + The view's vertical-group-by field. + """ + verticalGroupByFields( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the project v2 fields returned from the connection. + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConfigurationConnection + + """ + The view's visible fields. + """ + visibleFields( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the project v2 fields returned from the connection. + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConnection + @deprecated( + reason: "The `ProjectV2View#visibleFields` API is deprecated in favour of the more capable `ProjectV2View#fields` API. Check out the `ProjectV2View#fields` API as an example for the more capable alternative. Removal on 2023-01-01 UTC." + ) +} + +""" +The connection type for ProjectV2View. +""" +type ProjectV2ViewConnection { + """ + A list of edges. + """ + edges: [ProjectV2ViewEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2View] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2ViewEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2View +} + +""" +The layout of a project v2 view. +""" +enum ProjectV2ViewLayout { + """ + Board layout + """ + BOARD_LAYOUT + + """ + Roadmap layout + """ + ROADMAP_LAYOUT + + """ + Table layout + """ + TABLE_LAYOUT +} + +""" +Ordering options for project v2 view connections +""" +input ProjectV2ViewOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order the project v2 views by. + """ + field: ProjectV2ViewOrderField! +} + +""" +Properties by which project v2 view connections can be ordered. +""" +enum ProjectV2ViewOrderField { + """ + Order project v2 views by creation time + """ + CREATED_AT + + """ + Order project v2 views by name + """ + NAME + + """ + Order project v2 views by position + """ + POSITION +} + +""" +A workflow inside a project. +""" +type ProjectV2Workflow implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The workflows' enabled state. + """ + enabled: Boolean! + id: ID! + + """ + The workflows' name. + """ + name: String! + + """ + The workflows' number. + """ + number: Int! + + """ + The project that contains this workflow. + """ + project: ProjectV2! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for ProjectV2Workflow. +""" +type ProjectV2WorkflowConnection { + """ + A list of edges. + """ + edges: [ProjectV2WorkflowEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2Workflow] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2WorkflowEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2Workflow +} + +""" +Ordering options for project v2 workflows connections +""" +input ProjectV2WorkflowOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order the project v2 workflows by. + """ + field: ProjectV2WorkflowsOrderField! +} + +""" +Properties by which project workflows can be ordered. +""" +enum ProjectV2WorkflowsOrderField { + """ + The workflows' date and time of creation + """ + CREATED_AT + + """ + The workflows' name + """ + NAME + + """ + The workflows' number + """ + NUMBER + + """ + The workflows' date and time of update + """ + UPDATED_AT +} + +""" +A user's public key. +""" +type PublicKey implements Node { + """ + The last time this authorization was used to perform an action. Values will be null for keys not owned by the user. + """ + accessedAt: DateTime + + """ + Identifies the date and time when the key was created. Keys created before + March 5th, 2014 have inaccurate values. Values will be null for keys not owned by the user. + """ + createdAt: DateTime + + """ + The fingerprint for this PublicKey. + """ + fingerprint: String! + id: ID! + + """ + Whether this PublicKey is read-only or not. Values will be null for keys not owned by the user. + """ + isReadOnly: Boolean + + """ + The public key string. + """ + key: String! + + """ + Identifies the date and time when the key was updated. Keys created before + March 5th, 2014 may have inaccurate values. Values will be null for keys not + owned by the user. + """ + updatedAt: DateTime +} + +""" +The connection type for PublicKey. +""" +type PublicKeyConnection { + """ + A list of edges. + """ + edges: [PublicKeyEdge] + + """ + A list of nodes. + """ + nodes: [PublicKey] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PublicKeyEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PublicKey +} + +""" +Autogenerated input type of PublishSponsorsTier +""" +input PublishSponsorsTierInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the draft tier to publish. + """ + tierId: ID! @possibleTypes(concreteTypes: ["SponsorsTier"]) +} + +""" +Autogenerated return type of PublishSponsorsTier +""" +type PublishSponsorsTierPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The tier that was published. + """ + sponsorsTier: SponsorsTier +} + +""" +A repository pull request. +""" +type PullRequest implements Assignable & Closable & Comment & Labelable & Lockable & Node & ProjectV2Owner & Reactable & RepositoryNode & Subscribable & UniformResourceLocatable & Updatable & UpdatableComment { + """ + Reason that the conversation was locked. + """ + activeLockReason: LockReason + + """ + The number of additions in this pull request. + """ + additions: Int! + + """ + A list of Users assigned to this object. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Returns the auto-merge request object if one exists for this pull request. + """ + autoMergeRequest: AutoMergeRequest + + """ + Identifies the base Ref associated with the pull request. + """ + baseRef: Ref + + """ + Identifies the name of the base Ref associated with the pull request, even if the ref has been deleted. + """ + baseRefName: String! + + """ + Identifies the oid of the base ref associated with the pull request, even if the ref has been deleted. + """ + baseRefOid: GitObjectID! + + """ + The repository associated with this pull request's base Ref. + """ + baseRepository: Repository + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Whether or not the pull request is rebaseable. + """ + canBeRebased: Boolean! @preview(toggledBy: "merge-info-preview") + + """ + The number of changed files in this pull request. + """ + changedFiles: Int! + + """ + The HTTP path for the checks of this pull request. + """ + checksResourcePath: URI! + + """ + The HTTP URL for the checks of this pull request. + """ + checksUrl: URI! + + """ + `true` if the pull request is closed + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + List of issues that were may be closed by this pull request + """ + closingIssuesReferences( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection + """ + orderBy: IssueOrder + + """ + Return only manually linked Issues + """ + userLinkedOnly: Boolean = false + ): IssueConnection + + """ + A list of comments associated with the pull request. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issue comments returned from the connection. + """ + orderBy: IssueCommentOrder + ): IssueCommentConnection! + + """ + A list of commits present in this pull request's head branch not present in the base branch. + """ + commits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestCommitConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The number of deletions in this pull request. + """ + deletions: Int! + + """ + The actor who edited this pull request's body. + """ + editor: Actor + + """ + Lists the files changed within this pull request. + """ + files( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestChangedFileConnection + + """ + Identifies the head Ref associated with the pull request. + """ + headRef: Ref + + """ + Identifies the name of the head Ref associated with the pull request, even if the ref has been deleted. + """ + headRefName: String! + + """ + Identifies the oid of the head ref associated with the pull request, even if the ref has been deleted. + """ + headRefOid: GitObjectID! + + """ + The repository associated with this pull request's head Ref. + """ + headRepository: Repository + + """ + The owner of the repository associated with this pull request's head Ref. + """ + headRepositoryOwner: RepositoryOwner + + """ + The hovercard information for this issue + """ + hovercard( + """ + Whether or not to include notification contexts + """ + includeNotificationContexts: Boolean = true + ): Hovercard! + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + The head and base repositories are different. + """ + isCrossRepository: Boolean! + + """ + Identifies if the pull request is a draft. + """ + isDraft: Boolean! + + """ + Is this pull request read by the viewer + """ + isReadByViewer: Boolean + + """ + A list of labels associated with the object. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + A list of latest reviews per user associated with the pull request. + """ + latestOpinionatedReviews( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Only return reviews from user who have write access to the repository + """ + writersOnly: Boolean = false + ): PullRequestReviewConnection + + """ + A list of latest reviews per user associated with the pull request that are not also pending review. + """ + latestReviews( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestReviewConnection + + """ + `true` if the pull request is locked + """ + locked: Boolean! + + """ + Indicates whether maintainers can modify the pull request. + """ + maintainerCanModify: Boolean! + + """ + The commit that was created when this pull request was merged. + """ + mergeCommit: Commit + + """ + The merge queue entry of the pull request in the base branch's merge queue + """ + mergeQueueEntry: MergeQueueEntry + + """ + Detailed information about the current pull request merge state status. + """ + mergeStateStatus: MergeStateStatus! @preview(toggledBy: "merge-info-preview") + + """ + Whether or not the pull request can be merged based on the existence of merge conflicts. + """ + mergeable: MergeableState! + + """ + Whether or not the pull request was merged. + """ + merged: Boolean! + + """ + The date and time that the pull request was merged. + """ + mergedAt: DateTime + + """ + The actor who merged the pull request. + """ + mergedBy: Actor + + """ + Identifies the milestone associated with the pull request. + """ + milestone: Milestone + + """ + Identifies the pull request number. + """ + number: Int! + + """ + A list of Users that are participating in the Pull Request conversation. + """ + participants( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The permalink to the pull request. + """ + permalink: URI! + + """ + The commit that GitHub automatically generated to test if this pull request + could be merged. This field will not return a value if the pull request is + merged, or if the test merge commit is still being generated. See the + `mergeable` field for more details on the mergeability of the pull request. + """ + potentialMergeCommit: Commit + + """ + List of project cards associated with this pull request. + """ + projectCards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + A list of archived states to filter the cards by + """ + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectCardConnection! + + """ + List of project items associated with this pull request. + """ + projectItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Include archived items. + """ + includeArchived: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2ItemConnection! + + """ + Find a project by number. + """ + projectV2( + """ + The project number. + """ + number: Int! + ): ProjectV2 + + """ + A list of projects under the owner. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + A project to search for under the the owner. + """ + query: String + ): ProjectV2Connection! + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path for this pull request. + """ + resourcePath: URI! + + """ + The HTTP path for reverting this pull request. + """ + revertResourcePath: URI! + + """ + The HTTP URL for reverting this pull request. + """ + revertUrl: URI! + + """ + The current status of this pull request with respect to code review. + """ + reviewDecision: PullRequestReviewDecision + + """ + A list of review requests associated with the pull request. + """ + reviewRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ReviewRequestConnection + + """ + The list of all review threads for this pull request. + """ + reviewThreads( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestReviewThreadConnection! + + """ + A list of reviews associated with the pull request. + """ + reviews( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Filter by author of the review. + """ + author: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + A list of states to filter the reviews. + """ + states: [PullRequestReviewState!] + ): PullRequestReviewConnection + + """ + Identifies the state of the pull request. + """ + state: PullRequestState! + + """ + A list of reviewer suggestions based on commit history and past review comments. + """ + suggestedReviewers: [SuggestedReviewer]! + + """ + A list of events, comments, commits, etc. associated with the pull request. + """ + timeline( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows filtering timeline events by a `since` timestamp. + """ + since: DateTime + ): PullRequestTimelineConnection! + @deprecated(reason: "`timeline` will be removed Use PullRequest.timelineItems instead. Removal on 2020-10-01 UTC.") + + """ + A list of events, comments, commits, etc. associated with the pull request. + """ + timelineItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter timeline items by type. + """ + itemTypes: [PullRequestTimelineItemsItemType!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter timeline items by a `since` timestamp. + """ + since: DateTime + + """ + Skips the first _n_ elements in the list. + """ + skip: Int + ): PullRequestTimelineItemsConnection! + + """ + Identifies the pull request title. + """ + title: String! + + """ + Identifies the pull request title rendered to HTML. + """ + titleHTML: HTML! + + """ + Returns a count of how many comments this pull request has received. + """ + totalCommentsCount: Int + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this pull request. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Whether or not the viewer can apply suggestion. + """ + viewerCanApplySuggestion: Boolean! + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Check if the viewer can restore the deleted head ref. + """ + viewerCanDeleteHeadRef: Boolean! + + """ + Whether or not the viewer can disable auto-merge + """ + viewerCanDisableAutoMerge: Boolean! + + """ + Can the viewer edit files within this pull request. + """ + viewerCanEditFiles: Boolean! + + """ + Whether or not the viewer can enable auto-merge + """ + viewerCanEnableAutoMerge: Boolean! + + """ + Indicates whether the viewer can bypass branch protections and merge the pull request immediately + """ + viewerCanMergeAsAdmin: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Whether or not the viewer can update the head ref of this PR, by merging or rebasing the base ref. + If the head ref is up to date or unable to be updated by this user, this will return false. + """ + viewerCanUpdateBranch: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + The latest review given from the viewer. + """ + viewerLatestReview: PullRequestReview + + """ + The person who has requested the viewer for review on this pull request. + """ + viewerLatestReviewRequest: ReviewRequest + + """ + The merge body text for the viewer and method. + """ + viewerMergeBodyText( + """ + The merge method for the message. + """ + mergeType: PullRequestMergeMethod + ): String! + + """ + The merge headline text for the viewer and method. + """ + viewerMergeHeadlineText( + """ + The merge method for the message. + """ + mergeType: PullRequestMergeMethod + ): String! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +The possible methods for updating a pull request's head branch with the base branch. +""" +enum PullRequestBranchUpdateMethod { + """ + Update branch via merge + """ + MERGE + + """ + Update branch via rebase + """ + REBASE +} + +""" +A file changed in a pull request. +""" +type PullRequestChangedFile { + """ + The number of additions to the file. + """ + additions: Int! + + """ + How the file was changed in this PullRequest + """ + changeType: PatchStatus! + + """ + The number of deletions to the file. + """ + deletions: Int! + + """ + The path of the file. + """ + path: String! + + """ + The state of the file for the viewer. + """ + viewerViewedState: FileViewedState! +} + +""" +The connection type for PullRequestChangedFile. +""" +type PullRequestChangedFileConnection { + """ + A list of edges. + """ + edges: [PullRequestChangedFileEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestChangedFile] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PullRequestChangedFileEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestChangedFile +} + +""" +Represents a Git commit part of a pull request. +""" +type PullRequestCommit implements Node & UniformResourceLocatable { + """ + The Git commit object + """ + commit: Commit! + id: ID! + + """ + The pull request this commit belongs to + """ + pullRequest: PullRequest! + + """ + The HTTP path for this pull request commit + """ + resourcePath: URI! + + """ + The HTTP URL for this pull request commit + """ + url: URI! +} + +""" +Represents a commit comment thread part of a pull request. +""" +type PullRequestCommitCommentThread implements Node & RepositoryNode { + """ + The comments that exist in this thread. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + The commit the comments were made on. + """ + commit: Commit! + id: ID! + + """ + The file the comments were made on. + """ + path: String + + """ + The position in the diff for the commit that the comment was made on. + """ + position: Int + + """ + The pull request this commit comment thread belongs to + """ + pullRequest: PullRequest! + + """ + The repository associated with this node. + """ + repository: Repository! +} + +""" +The connection type for PullRequestCommit. +""" +type PullRequestCommitConnection { + """ + A list of edges. + """ + edges: [PullRequestCommitEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestCommit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PullRequestCommitEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestCommit +} + +""" +The connection type for PullRequest. +""" +type PullRequestConnection { + """ + A list of edges. + """ + edges: [PullRequestEdge] + + """ + A list of nodes. + """ + nodes: [PullRequest] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +This aggregates pull requests opened by a user within one repository. +""" +type PullRequestContributionsByRepository { + """ + The pull request contributions. + """ + contributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedPullRequestContributionConnection! + + """ + The repository in which the pull requests were opened. + """ + repository: Repository! +} + +""" +An edge in a connection. +""" +type PullRequestEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequest +} + +""" +Represents available types of methods to use when merging a pull request. +""" +enum PullRequestMergeMethod { + """ + Add all commits from the head branch to the base branch with a merge commit. + """ + MERGE + + """ + Add all commits from the head branch onto the base branch individually. + """ + REBASE + + """ + Combine all commits from the head branch into a single commit in the base branch. + """ + SQUASH +} + +""" +Ways in which lists of issues can be ordered upon return. +""" +input PullRequestOrder { + """ + The direction in which to order pull requests by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order pull requests by. + """ + field: PullRequestOrderField! +} + +""" +Properties by which pull_requests connections can be ordered. +""" +enum PullRequestOrderField { + """ + Order pull_requests by creation time + """ + CREATED_AT + + """ + Order pull_requests by update time + """ + UPDATED_AT +} + +""" +Require all commits be made to a non-target branch and submitted via a pull request before they can be merged. +""" +type PullRequestParameters { + """ + New, reviewable commits pushed will dismiss previous pull request review approvals. + """ + dismissStaleReviewsOnPush: Boolean! + + """ + Require an approving review in pull requests that modify files that have a designated code owner. + """ + requireCodeOwnerReview: Boolean! + + """ + Whether the most recent reviewable push must be approved by someone other than the person who pushed it. + """ + requireLastPushApproval: Boolean! + + """ + The number of approving reviews that are required before a pull request can be merged. + """ + requiredApprovingReviewCount: Int! + + """ + All conversations on code must be resolved before a pull request can be merged. + """ + requiredReviewThreadResolution: Boolean! +} + +""" +Require all commits be made to a non-target branch and submitted via a pull request before they can be merged. +""" +input PullRequestParametersInput { + """ + New, reviewable commits pushed will dismiss previous pull request review approvals. + """ + dismissStaleReviewsOnPush: Boolean! + + """ + Require an approving review in pull requests that modify files that have a designated code owner. + """ + requireCodeOwnerReview: Boolean! + + """ + Whether the most recent reviewable push must be approved by someone other than the person who pushed it. + """ + requireLastPushApproval: Boolean! + + """ + The number of approving reviews that are required before a pull request can be merged. + """ + requiredApprovingReviewCount: Int! + + """ + All conversations on code must be resolved before a pull request can be merged. + """ + requiredReviewThreadResolution: Boolean! +} + +""" +A review object for a given pull request. +""" +type PullRequestReview implements Comment & Deletable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Indicates whether the author of this review has push access to the repository. + """ + authorCanPushToRepository: Boolean! + + """ + Identifies the pull request review body. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body of this review rendered as plain text. + """ + bodyText: String! + + """ + A list of review comments for the current pull request review. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestReviewCommentConnection! + + """ + Identifies the commit associated with this pull request review. + """ + commit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + A list of teams that this review was made on behalf of. + """ + onBehalfOf( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): TeamConnection! + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + Identifies the pull request associated with this pull request review. + """ + pullRequest: PullRequest! + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path permalink for this PullRequestReview. + """ + resourcePath: URI! + + """ + Identifies the current state of the pull request review. + """ + state: PullRequestReviewState! + + """ + Identifies when the Pull Request Review was submitted + """ + submittedAt: DateTime + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL permalink for this PullRequestReview. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +A review comment associated with a given repository pull request. +""" +type PullRequestReviewComment implements Comment & Deletable & Minimizable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The comment body of this review comment. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The comment body of this review comment rendered as plain text. + """ + bodyText: String! + + """ + Identifies the commit associated with the comment. + """ + commit: Commit + + """ + Identifies when the comment was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The diff hunk to which the comment applies. + """ + diffHunk: String! + + """ + Identifies when the comment was created in a draft state. + """ + draftedAt: DateTime! + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + The end line number on the file to which the comment applies + """ + line: Int + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + Identifies the original commit associated with the comment. + """ + originalCommit: Commit + + """ + The end line number on the file to which the comment applied when it was first created + """ + originalLine: Int + + """ + The original line index in the diff to which the comment applies. + """ + originalPosition: Int! + @deprecated(reason: "We are phasing out diff-relative positioning for PR comments Removal on 2023-10-01 UTC.") + + """ + The start line number on the file to which the comment applied when it was first created + """ + originalStartLine: Int + + """ + Identifies when the comment body is outdated + """ + outdated: Boolean! + + """ + The path to which the comment applies. + """ + path: String! + + """ + The line index in the diff to which the comment applies. + """ + position: Int + @deprecated( + reason: "We are phasing out diff-relative positioning for PR comments Use the `line` and `startLine` fields instead, which are file line numbers instead of diff line numbers Removal on 2023-10-01 UTC." + ) + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + The pull request associated with this review comment. + """ + pullRequest: PullRequest! + + """ + The pull request review associated with this review comment. + """ + pullRequestReview: PullRequestReview + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The comment this is a reply to. + """ + replyTo: PullRequestReviewComment + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path permalink for this review comment. + """ + resourcePath: URI! + + """ + The start line number on the file to which the comment applies + """ + startLine: Int + + """ + Identifies the state of the comment. + """ + state: PullRequestReviewCommentState! + + """ + The level at which the comments in the corresponding thread are targeted, can be a diff line or a file + """ + subjectType: PullRequestReviewThreadSubjectType! + + """ + Identifies when the comment was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL permalink for this review comment. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for PullRequestReviewComment. +""" +type PullRequestReviewCommentConnection { + """ + A list of edges. + """ + edges: [PullRequestReviewCommentEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestReviewComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PullRequestReviewCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestReviewComment +} + +""" +The possible states of a pull request review comment. +""" +enum PullRequestReviewCommentState { + """ + A comment that is part of a pending review + """ + PENDING + + """ + A comment that is part of a submitted review + """ + SUBMITTED +} + +""" +The connection type for PullRequestReview. +""" +type PullRequestReviewConnection { + """ + A list of edges. + """ + edges: [PullRequestReviewEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestReview] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +This aggregates pull request reviews made by a user within one repository. +""" +type PullRequestReviewContributionsByRepository { + """ + The pull request review contributions. + """ + contributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedPullRequestReviewContributionConnection! + + """ + The repository in which the pull request reviews were made. + """ + repository: Repository! +} + +""" +The review status of a pull request. +""" +enum PullRequestReviewDecision { + """ + The pull request has received an approving review. + """ + APPROVED + + """ + Changes have been requested on the pull request. + """ + CHANGES_REQUESTED + + """ + A review is required before the pull request can be merged. + """ + REVIEW_REQUIRED +} + +""" +An edge in a connection. +""" +type PullRequestReviewEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestReview +} + +""" +The possible events to perform on a pull request review. +""" +enum PullRequestReviewEvent { + """ + Submit feedback and approve merging these changes. + """ + APPROVE + + """ + Submit general feedback without explicit approval. + """ + COMMENT + + """ + Dismiss review so it now longer effects merging. + """ + DISMISS + + """ + Submit feedback that must be addressed before merging. + """ + REQUEST_CHANGES +} + +""" +The possible states of a pull request review. +""" +enum PullRequestReviewState { + """ + A review allowing the pull request to merge. + """ + APPROVED + + """ + A review blocking the pull request from merging. + """ + CHANGES_REQUESTED + + """ + An informational review. + """ + COMMENTED + + """ + A review that has been dismissed. + """ + DISMISSED + + """ + A review that has not yet been submitted. + """ + PENDING +} + +""" +A threaded list of comments for a given pull request. +""" +type PullRequestReviewThread implements Node { + """ + A list of pull request comments associated with the thread. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Skips the first _n_ elements in the list. + """ + skip: Int + ): PullRequestReviewCommentConnection! + + """ + The side of the diff on which this thread was placed. + """ + diffSide: DiffSide! + id: ID! + + """ + Whether or not the thread has been collapsed (resolved) + """ + isCollapsed: Boolean! + + """ + Indicates whether this thread was outdated by newer changes. + """ + isOutdated: Boolean! + + """ + Whether this thread has been resolved + """ + isResolved: Boolean! + + """ + The line in the file to which this thread refers + """ + line: Int + + """ + The original line in the file to which this thread refers. + """ + originalLine: Int + + """ + The original start line in the file to which this thread refers (multi-line only). + """ + originalStartLine: Int + + """ + Identifies the file path of this thread. + """ + path: String! + + """ + Identifies the pull request associated with this thread. + """ + pullRequest: PullRequest! + + """ + Identifies the repository associated with this thread. + """ + repository: Repository! + + """ + The user who resolved this thread + """ + resolvedBy: User + + """ + The side of the diff that the first line of the thread starts on (multi-line only) + """ + startDiffSide: DiffSide + + """ + The start line in the file to which this thread refers (multi-line only) + """ + startLine: Int + + """ + The level at which the comments in the corresponding thread are targeted, can be a diff line or a file + """ + subjectType: PullRequestReviewThreadSubjectType! + + """ + Indicates whether the current viewer can reply to this thread. + """ + viewerCanReply: Boolean! + + """ + Whether or not the viewer can resolve this thread + """ + viewerCanResolve: Boolean! + + """ + Whether or not the viewer can unresolve this thread + """ + viewerCanUnresolve: Boolean! +} + +""" +Review comment threads for a pull request review. +""" +type PullRequestReviewThreadConnection { + """ + A list of edges. + """ + edges: [PullRequestReviewThreadEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestReviewThread] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PullRequestReviewThreadEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestReviewThread +} + +""" +The possible subject types of a pull request review comment. +""" +enum PullRequestReviewThreadSubjectType { + """ + A comment that has been made against the file of a pull request + """ + FILE + + """ + A comment that has been made against the line of a pull request + """ + LINE +} + +""" +Represents the latest point in the pull request timeline for which the viewer has seen the pull request's commits. +""" +type PullRequestRevisionMarker { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The last commit the viewer has seen. + """ + lastSeenCommit: Commit! + + """ + The pull request to which the marker belongs. + """ + pullRequest: PullRequest! +} + +""" +The possible states of a pull request. +""" +enum PullRequestState { + """ + A pull request that has been closed without being merged. + """ + CLOSED + + """ + A pull request that has been closed by being merged. + """ + MERGED + + """ + A pull request that is still open. + """ + OPEN +} + +""" +A repository pull request template. +""" +type PullRequestTemplate { + """ + The body of the template + """ + body: String + + """ + The filename of the template + """ + filename: String + + """ + The repository the template belongs to + """ + repository: Repository! +} + +""" +A threaded list of comments for a given pull request. +""" +type PullRequestThread implements Node { + """ + A list of pull request comments associated with the thread. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Skips the first _n_ elements in the list. + """ + skip: Int + ): PullRequestReviewCommentConnection! + + """ + The side of the diff on which this thread was placed. + """ + diffSide: DiffSide! + id: ID! + + """ + Whether or not the thread has been collapsed (resolved) + """ + isCollapsed: Boolean! + + """ + Indicates whether this thread was outdated by newer changes. + """ + isOutdated: Boolean! + + """ + Whether this thread has been resolved + """ + isResolved: Boolean! + + """ + The line in the file to which this thread refers + """ + line: Int + + """ + Identifies the file path of this thread. + """ + path: String! + + """ + Identifies the pull request associated with this thread. + """ + pullRequest: PullRequest! + + """ + Identifies the repository associated with this thread. + """ + repository: Repository! + + """ + The user who resolved this thread + """ + resolvedBy: User + + """ + The side of the diff that the first line of the thread starts on (multi-line only) + """ + startDiffSide: DiffSide + + """ + The line of the first file diff in the thread. + """ + startLine: Int + + """ + The level at which the comments in the corresponding thread are targeted, can be a diff line or a file + """ + subjectType: PullRequestReviewThreadSubjectType! + + """ + Indicates whether the current viewer can reply to this thread. + """ + viewerCanReply: Boolean! + + """ + Whether or not the viewer can resolve this thread + """ + viewerCanResolve: Boolean! + + """ + Whether or not the viewer can unresolve this thread + """ + viewerCanUnresolve: Boolean! +} + +""" +The connection type for PullRequestTimelineItem. +""" +type PullRequestTimelineConnection { + """ + A list of edges. + """ + edges: [PullRequestTimelineItemEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestTimelineItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An item in a pull request timeline +""" +union PullRequestTimelineItem = + AssignedEvent + | BaseRefDeletedEvent + | BaseRefForcePushedEvent + | ClosedEvent + | Commit + | CommitCommentThread + | CrossReferencedEvent + | DemilestonedEvent + | DeployedEvent + | DeploymentEnvironmentChangedEvent + | HeadRefDeletedEvent + | HeadRefForcePushedEvent + | HeadRefRestoredEvent + | IssueComment + | LabeledEvent + | LockedEvent + | MergedEvent + | MilestonedEvent + | PullRequestReview + | PullRequestReviewComment + | PullRequestReviewThread + | ReferencedEvent + | RenamedTitleEvent + | ReopenedEvent + | ReviewDismissedEvent + | ReviewRequestRemovedEvent + | ReviewRequestedEvent + | SubscribedEvent + | UnassignedEvent + | UnlabeledEvent + | UnlockedEvent + | UnsubscribedEvent + | UserBlockedEvent + +""" +An edge in a connection. +""" +type PullRequestTimelineItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestTimelineItem +} + +""" +An item in a pull request timeline +""" +union PullRequestTimelineItems = + AddedToMergeQueueEvent + | AddedToProjectEvent + | AssignedEvent + | AutoMergeDisabledEvent + | AutoMergeEnabledEvent + | AutoRebaseEnabledEvent + | AutoSquashEnabledEvent + | AutomaticBaseChangeFailedEvent + | AutomaticBaseChangeSucceededEvent + | BaseRefChangedEvent + | BaseRefDeletedEvent + | BaseRefForcePushedEvent + | ClosedEvent + | CommentDeletedEvent + | ConnectedEvent + | ConvertToDraftEvent + | ConvertedNoteToIssueEvent + | ConvertedToDiscussionEvent + | CrossReferencedEvent + | DemilestonedEvent + | DeployedEvent + | DeploymentEnvironmentChangedEvent + | DisconnectedEvent + | HeadRefDeletedEvent + | HeadRefForcePushedEvent + | HeadRefRestoredEvent + | IssueComment + | LabeledEvent + | LockedEvent + | MarkedAsDuplicateEvent + | MentionedEvent + | MergedEvent + | MilestonedEvent + | MovedColumnsInProjectEvent + | PinnedEvent + | PullRequestCommit + | PullRequestCommitCommentThread + | PullRequestReview + | PullRequestReviewThread + | PullRequestRevisionMarker + | ReadyForReviewEvent + | ReferencedEvent + | RemovedFromMergeQueueEvent + | RemovedFromProjectEvent + | RenamedTitleEvent + | ReopenedEvent + | ReviewDismissedEvent + | ReviewRequestRemovedEvent + | ReviewRequestedEvent + | SubscribedEvent + | TransferredEvent + | UnassignedEvent + | UnlabeledEvent + | UnlockedEvent + | UnmarkedAsDuplicateEvent + | UnpinnedEvent + | UnsubscribedEvent + | UserBlockedEvent + +""" +The connection type for PullRequestTimelineItems. +""" +type PullRequestTimelineItemsConnection { + """ + A list of edges. + """ + edges: [PullRequestTimelineItemsEdge] + + """ + Identifies the count of items after applying `before` and `after` filters. + """ + filteredCount: Int! + + """ + A list of nodes. + """ + nodes: [PullRequestTimelineItems] + + """ + Identifies the count of items after applying `before`/`after` filters and `first`/`last`/`skip` slicing. + """ + pageCount: Int! + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Identifies the date and time when the timeline was last updated. + """ + updatedAt: DateTime! +} + +""" +An edge in a connection. +""" +type PullRequestTimelineItemsEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestTimelineItems +} + +""" +The possible item types found in a timeline. +""" +enum PullRequestTimelineItemsItemType { + """ + Represents an 'added_to_merge_queue' event on a given pull request. + """ + ADDED_TO_MERGE_QUEUE_EVENT + + """ + Represents a 'added_to_project' event on a given issue or pull request. + """ + ADDED_TO_PROJECT_EVENT + + """ + Represents an 'assigned' event on any assignable object. + """ + ASSIGNED_EVENT + + """ + Represents a 'automatic_base_change_failed' event on a given pull request. + """ + AUTOMATIC_BASE_CHANGE_FAILED_EVENT + + """ + Represents a 'automatic_base_change_succeeded' event on a given pull request. + """ + AUTOMATIC_BASE_CHANGE_SUCCEEDED_EVENT + + """ + Represents a 'auto_merge_disabled' event on a given pull request. + """ + AUTO_MERGE_DISABLED_EVENT + + """ + Represents a 'auto_merge_enabled' event on a given pull request. + """ + AUTO_MERGE_ENABLED_EVENT + + """ + Represents a 'auto_rebase_enabled' event on a given pull request. + """ + AUTO_REBASE_ENABLED_EVENT + + """ + Represents a 'auto_squash_enabled' event on a given pull request. + """ + AUTO_SQUASH_ENABLED_EVENT + + """ + Represents a 'base_ref_changed' event on a given issue or pull request. + """ + BASE_REF_CHANGED_EVENT + + """ + Represents a 'base_ref_deleted' event on a given pull request. + """ + BASE_REF_DELETED_EVENT + + """ + Represents a 'base_ref_force_pushed' event on a given pull request. + """ + BASE_REF_FORCE_PUSHED_EVENT + + """ + Represents a 'closed' event on any `Closable`. + """ + CLOSED_EVENT + + """ + Represents a 'comment_deleted' event on a given issue or pull request. + """ + COMMENT_DELETED_EVENT + + """ + Represents a 'connected' event on a given issue or pull request. + """ + CONNECTED_EVENT + + """ + Represents a 'converted_note_to_issue' event on a given issue or pull request. + """ + CONVERTED_NOTE_TO_ISSUE_EVENT + + """ + Represents a 'converted_to_discussion' event on a given issue. + """ + CONVERTED_TO_DISCUSSION_EVENT + + """ + Represents a 'convert_to_draft' event on a given pull request. + """ + CONVERT_TO_DRAFT_EVENT + + """ + Represents a mention made by one issue or pull request to another. + """ + CROSS_REFERENCED_EVENT + + """ + Represents a 'demilestoned' event on a given issue or pull request. + """ + DEMILESTONED_EVENT + + """ + Represents a 'deployed' event on a given pull request. + """ + DEPLOYED_EVENT + + """ + Represents a 'deployment_environment_changed' event on a given pull request. + """ + DEPLOYMENT_ENVIRONMENT_CHANGED_EVENT + + """ + Represents a 'disconnected' event on a given issue or pull request. + """ + DISCONNECTED_EVENT + + """ + Represents a 'head_ref_deleted' event on a given pull request. + """ + HEAD_REF_DELETED_EVENT + + """ + Represents a 'head_ref_force_pushed' event on a given pull request. + """ + HEAD_REF_FORCE_PUSHED_EVENT + + """ + Represents a 'head_ref_restored' event on a given pull request. + """ + HEAD_REF_RESTORED_EVENT + + """ + Represents a comment on an Issue. + """ + ISSUE_COMMENT + + """ + Represents a 'labeled' event on a given issue or pull request. + """ + LABELED_EVENT + + """ + Represents a 'locked' event on a given issue or pull request. + """ + LOCKED_EVENT + + """ + Represents a 'marked_as_duplicate' event on a given issue or pull request. + """ + MARKED_AS_DUPLICATE_EVENT + + """ + Represents a 'mentioned' event on a given issue or pull request. + """ + MENTIONED_EVENT + + """ + Represents a 'merged' event on a given pull request. + """ + MERGED_EVENT + + """ + Represents a 'milestoned' event on a given issue or pull request. + """ + MILESTONED_EVENT + + """ + Represents a 'moved_columns_in_project' event on a given issue or pull request. + """ + MOVED_COLUMNS_IN_PROJECT_EVENT + + """ + Represents a 'pinned' event on a given issue or pull request. + """ + PINNED_EVENT + + """ + Represents a Git commit part of a pull request. + """ + PULL_REQUEST_COMMIT + + """ + Represents a commit comment thread part of a pull request. + """ + PULL_REQUEST_COMMIT_COMMENT_THREAD + + """ + A review object for a given pull request. + """ + PULL_REQUEST_REVIEW + + """ + A threaded list of comments for a given pull request. + """ + PULL_REQUEST_REVIEW_THREAD + + """ + Represents the latest point in the pull request timeline for which the viewer has seen the pull request's commits. + """ + PULL_REQUEST_REVISION_MARKER + + """ + Represents a 'ready_for_review' event on a given pull request. + """ + READY_FOR_REVIEW_EVENT + + """ + Represents a 'referenced' event on a given `ReferencedSubject`. + """ + REFERENCED_EVENT + + """ + Represents a 'removed_from_merge_queue' event on a given pull request. + """ + REMOVED_FROM_MERGE_QUEUE_EVENT + + """ + Represents a 'removed_from_project' event on a given issue or pull request. + """ + REMOVED_FROM_PROJECT_EVENT + + """ + Represents a 'renamed' event on a given issue or pull request + """ + RENAMED_TITLE_EVENT + + """ + Represents a 'reopened' event on any `Closable`. + """ + REOPENED_EVENT + + """ + Represents a 'review_dismissed' event on a given issue or pull request. + """ + REVIEW_DISMISSED_EVENT + + """ + Represents an 'review_requested' event on a given pull request. + """ + REVIEW_REQUESTED_EVENT + + """ + Represents an 'review_request_removed' event on a given pull request. + """ + REVIEW_REQUEST_REMOVED_EVENT + + """ + Represents a 'subscribed' event on a given `Subscribable`. + """ + SUBSCRIBED_EVENT + + """ + Represents a 'transferred' event on a given issue or pull request. + """ + TRANSFERRED_EVENT + + """ + Represents an 'unassigned' event on any assignable object. + """ + UNASSIGNED_EVENT + + """ + Represents an 'unlabeled' event on a given issue or pull request. + """ + UNLABELED_EVENT + + """ + Represents an 'unlocked' event on a given issue or pull request. + """ + UNLOCKED_EVENT + + """ + Represents an 'unmarked_as_duplicate' event on a given issue or pull request. + """ + UNMARKED_AS_DUPLICATE_EVENT + + """ + Represents an 'unpinned' event on a given issue or pull request. + """ + UNPINNED_EVENT + + """ + Represents an 'unsubscribed' event on a given `Subscribable`. + """ + UNSUBSCRIBED_EVENT + + """ + Represents a 'user_blocked' event on a given user. + """ + USER_BLOCKED_EVENT +} + +""" +The possible target states when updating a pull request. +""" +enum PullRequestUpdateState { + """ + A pull request that has been closed without being merged. + """ + CLOSED + + """ + A pull request that is still open. + """ + OPEN +} + +""" +A Git push. +""" +type Push implements Node { + id: ID! + + """ + The SHA after the push + """ + nextSha: GitObjectID + + """ + The permalink for this push. + """ + permalink: URI! + + """ + The SHA before the push + """ + previousSha: GitObjectID + + """ + The actor who pushed + """ + pusher: Actor! + + """ + The repository that was pushed to + """ + repository: Repository! +} + +""" +A team, user, or app who has the ability to push to a protected branch. +""" +type PushAllowance implements Node { + """ + The actor that can push. + """ + actor: PushAllowanceActor + + """ + Identifies the branch protection rule associated with the allowed user, team, or app. + """ + branchProtectionRule: BranchProtectionRule + id: ID! +} + +""" +Types that can be an actor. +""" +union PushAllowanceActor = App | Team | User + +""" +The connection type for PushAllowance. +""" +type PushAllowanceConnection { + """ + A list of edges. + """ + edges: [PushAllowanceEdge] + + """ + A list of nodes. + """ + nodes: [PushAllowance] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PushAllowanceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PushAllowance +} + +""" +The query root of GitHub's GraphQL interface. +""" +type Query { + """ + Look up a code of conduct by its key + """ + codeOfConduct( + """ + The code of conduct's key + """ + key: String! + ): CodeOfConduct + + """ + Look up a code of conduct by its key + """ + codesOfConduct: [CodeOfConduct] + + """ + Look up an enterprise by URL slug. + """ + enterprise( + """ + The enterprise invitation token. + """ + invitationToken: String + + """ + The enterprise URL slug. + """ + slug: String! + ): Enterprise + + """ + Look up a pending enterprise administrator invitation by invitee, enterprise and role. + """ + enterpriseAdministratorInvitation( + """ + The slug of the enterprise the user was invited to join. + """ + enterpriseSlug: String! + + """ + The role for the business member invitation. + """ + role: EnterpriseAdministratorRole! + + """ + The login of the user invited to join the business. + """ + userLogin: String! + ): EnterpriseAdministratorInvitation + + """ + Look up a pending enterprise administrator invitation by invitation token. + """ + enterpriseAdministratorInvitationByToken( + """ + The invitation token sent with the invitation email. + """ + invitationToken: String! + ): EnterpriseAdministratorInvitation + + """ + Look up an open source license by its key + """ + license( + """ + The license's downcased SPDX ID + """ + key: String! + ): License + + """ + Return a list of known open source licenses + """ + licenses: [License]! + + """ + Get alphabetically sorted list of Marketplace categories + """ + marketplaceCategories( + """ + Exclude categories with no listings. + """ + excludeEmpty: Boolean + + """ + Returns top level categories only, excluding any subcategories. + """ + excludeSubcategories: Boolean + + """ + Return only the specified categories. + """ + includeCategories: [String!] + ): [MarketplaceCategory!]! + + """ + Look up a Marketplace category by its slug. + """ + marketplaceCategory( + """ + The URL slug of the category. + """ + slug: String! + + """ + Also check topic aliases for the category slug + """ + useTopicAliases: Boolean + ): MarketplaceCategory + + """ + Look up a single Marketplace listing + """ + marketplaceListing( + """ + Select the listing that matches this slug. It's the short name of the listing used in its URL. + """ + slug: String! + ): MarketplaceListing + + """ + Look up Marketplace listings + """ + marketplaceListings( + """ + Select listings that can be administered by the specified user. + """ + adminId: ID + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Select listings visible to the viewer even if they are not approved. If omitted or + false, only approved listings will be returned. + """ + allStates: Boolean + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Select only listings with the given category. + """ + categorySlug: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Select listings for products owned by the specified organization. + """ + organizationId: ID + + """ + Select only listings where the primary category matches the given category slug. + """ + primaryCategoryOnly: Boolean = false + + """ + Select the listings with these slugs, if they are visible to the viewer. + """ + slugs: [String] + + """ + Also check topic aliases for the category slug + """ + useTopicAliases: Boolean + + """ + Select listings to which user has admin access. If omitted, listings visible to the + viewer are returned. + """ + viewerCanAdmin: Boolean + + """ + Select only listings that offer a free trial. + """ + withFreeTrialsOnly: Boolean = false + ): MarketplaceListingConnection! + + """ + Return information about the GitHub instance + """ + meta: GitHubMetadata! + + """ + Fetches an object given its ID. + """ + node( + """ + ID of the object. + """ + id: ID! + ): Node + + """ + Lookup nodes by a list of IDs. + """ + nodes( + """ + The list of node IDs. + """ + ids: [ID!]! + ): [Node]! + + """ + Lookup a organization by login. + """ + organization( + """ + The organization's login. + """ + login: String! + ): Organization + + """ + The client's rate limit information. + """ + rateLimit( + """ + If true, calculate the cost for the query without evaluating it + """ + dryRun: Boolean = false + ): RateLimit + + """ + Workaround for re-exposing the root query object. (Refer to + https://github.com/facebook/relay/issues/112 for more information.) + """ + relay: Query! + + """ + Lookup a given repository by the owner and repository name. + """ + repository( + """ + Follow repository renames. If disabled, a repository referenced by its old name will return an error. + """ + followRenames: Boolean = true + + """ + The name of the repository + """ + name: String! + + """ + The login field of a user or organization + """ + owner: String! + ): Repository + + """ + Lookup a repository owner (ie. either a User or an Organization) by login. + """ + repositoryOwner( + """ + The username to lookup the owner by. + """ + login: String! + ): RepositoryOwner + + """ + Lookup resource by a URL. + """ + resource( + """ + The URL. + """ + url: URI! + ): UniformResourceLocatable + + """ + Perform a search across resources, returning a maximum of 1,000 results. + """ + search( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The search string to look for. + """ + query: String! + + """ + The types of search items to search within. + """ + type: SearchType! + ): SearchResultItemConnection! + + """ + GitHub Security Advisories + """ + securityAdvisories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + A list of classifications to filter advisories by. + """ + classifications: [SecurityAdvisoryClassification!] + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter advisories by identifier, e.g. GHSA or CVE. + """ + identifier: SecurityAdvisoryIdentifierFilter + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the returned topics. + """ + orderBy: SecurityAdvisoryOrder = {field: UPDATED_AT, direction: DESC} + + """ + Filter advisories to those published since a time in the past. + """ + publishedSince: DateTime + + """ + Filter advisories to those updated since a time in the past. + """ + updatedSince: DateTime + ): SecurityAdvisoryConnection! + + """ + Fetch a Security Advisory by its GHSA ID + """ + securityAdvisory( + """ + GitHub Security Advisory ID. + """ + ghsaId: String! + ): SecurityAdvisory + + """ + Software Vulnerabilities documented by GitHub Security Advisories + """ + securityVulnerabilities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + A list of advisory classifications to filter vulnerabilities by. + """ + classifications: [SecurityAdvisoryClassification!] + + """ + An ecosystem to filter vulnerabilities by. + """ + ecosystem: SecurityAdvisoryEcosystem + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the returned topics. + """ + orderBy: SecurityVulnerabilityOrder = {field: UPDATED_AT, direction: DESC} + + """ + A package name to filter vulnerabilities by. + """ + package: String + + """ + A list of severities to filter vulnerabilities by. + """ + severities: [SecurityAdvisorySeverity!] + ): SecurityVulnerabilityConnection! + + """ + Users and organizations who can be sponsored via GitHub Sponsors. + """ + sponsorables( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Optional filter for which dependencies should be checked for sponsorable + owners. Only sponsorable owners of dependencies in this ecosystem will be + included. Used when onlyDependencies = true. + + **Upcoming Change on 2022-07-01 UTC** + **Description:** `dependencyEcosystem` will be removed. Use the ecosystem argument instead. + **Reason:** The type is switching from SecurityAdvisoryEcosystem to DependencyGraphEcosystem. + """ + dependencyEcosystem: SecurityAdvisoryEcosystem + + """ + Optional filter for which dependencies should be checked for sponsorable + owners. Only sponsorable owners of dependencies in this ecosystem will be + included. Used when onlyDependencies = true. + """ + ecosystem: DependencyGraphEcosystem + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Whether only sponsorables who own the viewer's dependencies will be + returned. Must be authenticated to use. Can check an organization instead + for their dependencies owned by sponsorables by passing + orgLoginForDependencies. + """ + onlyDependencies: Boolean = false + + """ + Ordering options for users and organizations returned from the connection. + """ + orderBy: SponsorableOrder = {field: LOGIN, direction: ASC} + + """ + Optional organization username for whose dependencies should be checked. + Used when onlyDependencies = true. Omit to check your own dependencies. If + you are not an administrator of the organization, only dependencies from its + public repositories will be considered. + """ + orgLoginForDependencies: String + ): SponsorableItemConnection! + + """ + Look up a topic by name. + """ + topic( + """ + The topic's name. + """ + name: String! + ): Topic + + """ + Lookup a user by login. + """ + user( + """ + The user's login. + """ + login: String! + ): User + + """ + The currently authenticated user. + """ + viewer: User! +} + +""" +Represents the client's rate limit. +""" +type RateLimit { + """ + The point cost for the current query counting against the rate limit. + """ + cost: Int! + + """ + The maximum number of points the client is permitted to consume in a 60 minute window. + """ + limit: Int! + + """ + The maximum number of nodes this query may return + """ + nodeCount: Int! + + """ + The number of points remaining in the current rate limit window. + """ + remaining: Int! + + """ + The time at which the current rate limit window resets in UTC epoch seconds. + """ + resetAt: DateTime! + + """ + The number of points used in the current rate limit window. + """ + used: Int! +} + +""" +Represents a subject that can be reacted on. +""" +interface Reactable { + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! +} + +""" +The connection type for User. +""" +type ReactingUserConnection { + """ + A list of edges. + """ + edges: [ReactingUserEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user that's made a reaction. +""" +type ReactingUserEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: User! + + """ + The moment when the user made the reaction. + """ + reactedAt: DateTime! +} + +""" +An emoji reaction to a particular piece of content. +""" +type Reaction implements Node { + """ + Identifies the emoji reaction. + """ + content: ReactionContent! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The reactable piece of content + """ + reactable: Reactable! + + """ + Identifies the user who created this reaction. + """ + user: User +} + +""" +A list of reactions that have been left on the subject. +""" +type ReactionConnection { + """ + A list of edges. + """ + edges: [ReactionEdge] + + """ + A list of nodes. + """ + nodes: [Reaction] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Whether or not the authenticated user has left a reaction on the subject. + """ + viewerHasReacted: Boolean! +} + +""" +Emojis that can be attached to Issues, Pull Requests and Comments. +""" +enum ReactionContent { + """ + Represents the `:confused:` emoji. + """ + CONFUSED + + """ + Represents the `:eyes:` emoji. + """ + EYES + + """ + Represents the `:heart:` emoji. + """ + HEART + + """ + Represents the `:hooray:` emoji. + """ + HOORAY + + """ + Represents the `:laugh:` emoji. + """ + LAUGH + + """ + Represents the `:rocket:` emoji. + """ + ROCKET + + """ + Represents the `:-1:` emoji. + """ + THUMBS_DOWN + + """ + Represents the `:+1:` emoji. + """ + THUMBS_UP +} + +""" +An edge in a connection. +""" +type ReactionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Reaction +} + +""" +A group of emoji reactions to a particular piece of content. +""" +type ReactionGroup { + """ + Identifies the emoji reaction. + """ + content: ReactionContent! + + """ + Identifies when the reaction was created. + """ + createdAt: DateTime + + """ + Reactors to the reaction subject with the emotion represented by this reaction group. + """ + reactors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ReactorConnection! + + """ + The subject that was reacted to. + """ + subject: Reactable! + + """ + Users who have reacted to the reaction subject with the emotion represented by this reaction group + """ + users( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ReactingUserConnection! + @deprecated( + reason: "Reactors can now be mannequins, bots, and organizations. Use the `reactors` field instead. Removal on 2021-10-01 UTC." + ) + + """ + Whether or not the authenticated user has left a reaction on the subject. + """ + viewerHasReacted: Boolean! +} + +""" +Ways in which lists of reactions can be ordered upon return. +""" +input ReactionOrder { + """ + The direction in which to order reactions by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order reactions by. + """ + field: ReactionOrderField! +} + +""" +A list of fields that reactions can be ordered by. +""" +enum ReactionOrderField { + """ + Allows ordering a list of reactions by when they were created. + """ + CREATED_AT +} + +""" +Types that can be assigned to reactions. +""" +union Reactor = Bot | Mannequin | Organization | User + +""" +The connection type for Reactor. +""" +type ReactorConnection { + """ + A list of edges. + """ + edges: [ReactorEdge] + + """ + A list of nodes. + """ + nodes: [Reactor] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents an author of a reaction. +""" +type ReactorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The author of the reaction. + """ + node: Reactor! + + """ + The moment when the user made the reaction. + """ + reactedAt: DateTime! +} + +""" +Represents a 'ready_for_review' event on a given pull request. +""" +type ReadyForReviewEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + The HTTP path for this ready for review event. + """ + resourcePath: URI! + + """ + The HTTP URL for this ready for review event. + """ + url: URI! +} + +""" +Represents a Git reference. +""" +type Ref implements Node { + """ + A list of pull requests with this ref as the head ref. + """ + associatedPullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + Branch protection rules for this ref + """ + branchProtectionRule: BranchProtectionRule + + """ + Compares the current ref as a base ref to another head ref, if the comparison can be made. + """ + compare( + """ + The head ref to compare against. + """ + headRef: String! + ): Comparison + id: ID! + + """ + The ref name. + """ + name: String! + + """ + The ref's prefix, such as `refs/heads/` or `refs/tags/`. + """ + prefix: String! + + """ + Branch protection rules that are viewable by non-admins + """ + refUpdateRule: RefUpdateRule + + """ + The repository the ref belongs to. + """ + repository: Repository! + + """ + The object the ref points to. Returns null when object does not exist. + """ + target: GitObject +} + +""" +The connection type for Ref. +""" +type RefConnection { + """ + A list of edges. + """ + edges: [RefEdge] + + """ + A list of nodes. + """ + nodes: [Ref] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RefEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Ref +} + +""" +Parameters to be used for the ref_name condition +""" +type RefNameConditionTarget { + """ + Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match. + """ + exclude: [String!]! + + """ + Array of ref names or patterns to include. One of these patterns must match + for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the + default branch or `~ALL` to include all branches. + """ + include: [String!]! +} + +""" +Parameters to be used for the ref_name condition +""" +input RefNameConditionTargetInput { + """ + Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match. + """ + exclude: [String!]! + + """ + Array of ref names or patterns to include. One of these patterns must match + for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the + default branch or `~ALL` to include all branches. + """ + include: [String!]! +} + +""" +Ways in which lists of git refs can be ordered upon return. +""" +input RefOrder { + """ + The direction in which to order refs by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order refs by. + """ + field: RefOrderField! +} + +""" +Properties by which ref connections can be ordered. +""" +enum RefOrderField { + """ + Order refs by their alphanumeric name + """ + ALPHABETICAL + + """ + Order refs by underlying commit date if the ref prefix is refs/tags/ + """ + TAG_COMMIT_DATE +} + +""" +A ref update +""" +input RefUpdate @preview(toggledBy: "update-refs-preview") { + """ + The value this ref should be updated to. + """ + afterOid: GitObjectID! + + """ + The value this ref needs to point to before the update. + """ + beforeOid: GitObjectID + + """ + Force a non fast-forward update. + """ + force: Boolean = false + + """ + The fully qualified name of the ref to be update. For example `refs/heads/branch-name` + """ + name: GitRefname! +} + +""" +A ref update rules for a viewer. +""" +type RefUpdateRule { + """ + Can this branch be deleted. + """ + allowsDeletions: Boolean! + + """ + Are force pushes allowed on this branch. + """ + allowsForcePushes: Boolean! + + """ + Can matching branches be created. + """ + blocksCreations: Boolean! + + """ + Identifies the protection rule pattern. + """ + pattern: String! + + """ + Number of approving reviews required to update matching branches. + """ + requiredApprovingReviewCount: Int + + """ + List of required status check contexts that must pass for commits to be accepted to matching branches. + """ + requiredStatusCheckContexts: [String] + + """ + Are reviews from code owners required to update matching branches. + """ + requiresCodeOwnerReviews: Boolean! + + """ + Are conversations required to be resolved before merging. + """ + requiresConversationResolution: Boolean! + + """ + Are merge commits prohibited from being pushed to this branch. + """ + requiresLinearHistory: Boolean! + + """ + Are commits required to be signed. + """ + requiresSignatures: Boolean! + + """ + Is the viewer allowed to dismiss reviews. + """ + viewerAllowedToDismissReviews: Boolean! + + """ + Can the viewer push to the branch + """ + viewerCanPush: Boolean! +} + +""" +Represents a 'referenced' event on a given `ReferencedSubject`. +""" +type ReferencedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the commit associated with the 'referenced' event. + """ + commit: Commit + + """ + Identifies the repository associated with the 'referenced' event. + """ + commitRepository: Repository! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Reference originated in a different repository. + """ + isCrossRepository: Boolean! + + """ + Checks if the commit message itself references the subject. Can be false in the case of a commit comment reference. + """ + isDirectReference: Boolean! + + """ + Object referenced by event. + """ + subject: ReferencedSubject! +} + +""" +Any referencable object +""" +union ReferencedSubject = Issue | PullRequest + +""" +Autogenerated input type of RegenerateEnterpriseIdentityProviderRecoveryCodes +""" +input RegenerateEnterpriseIdentityProviderRecoveryCodesInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set an identity provider. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) +} + +""" +Autogenerated return type of RegenerateEnterpriseIdentityProviderRecoveryCodes +""" +type RegenerateEnterpriseIdentityProviderRecoveryCodesPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The identity provider for the enterprise. + """ + identityProvider: EnterpriseIdentityProvider +} + +""" +Autogenerated input type of RegenerateVerifiableDomainToken +""" +input RegenerateVerifiableDomainTokenInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the verifiable domain to regenerate the verification token of. + """ + id: ID! @possibleTypes(concreteTypes: ["VerifiableDomain"]) +} + +""" +Autogenerated return type of RegenerateVerifiableDomainToken +""" +type RegenerateVerifiableDomainTokenPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The verification token that was generated. + """ + verificationToken: String +} + +""" +Autogenerated input type of RejectDeployments +""" +input RejectDeploymentsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Optional comment for rejecting deployments + """ + comment: String = "" + + """ + The ids of environments to reject deployments + """ + environmentIds: [ID!]! + + """ + The node ID of the workflow run containing the pending deployments. + """ + workflowRunId: ID! @possibleTypes(concreteTypes: ["WorkflowRun"]) +} + +""" +Autogenerated return type of RejectDeployments +""" +type RejectDeploymentsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The affected deployments. + """ + deployments: [Deployment!] +} + +""" +A release contains the content for a release. +""" +type Release implements Node & Reactable & UniformResourceLocatable { + """ + The author of the release + """ + author: User + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The description of the release. + """ + description: String + + """ + The description of this release rendered to HTML. + """ + descriptionHTML: HTML + id: ID! + + """ + Whether or not the release is a draft + """ + isDraft: Boolean! + + """ + Whether or not the release is the latest releast + """ + isLatest: Boolean! + + """ + Whether or not the release is a prerelease + """ + isPrerelease: Boolean! + + """ + A list of users mentioned in the release description + """ + mentions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection + + """ + The title of the release. + """ + name: String + + """ + Identifies the date and time when the release was created. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + List of releases assets which are dependent on this release. + """ + releaseAssets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + A list of names to filter the assets by. + """ + name: String + ): ReleaseAssetConnection! + + """ + The repository that the release belongs to. + """ + repository: Repository! + + """ + The HTTP path for this issue + """ + resourcePath: URI! + + """ + A description of the release, rendered to HTML without any links in it. + """ + shortDescriptionHTML( + """ + How many characters to return. + """ + limit: Int = 200 + ): HTML + + """ + The Git tag the release points to + """ + tag: Ref + + """ + The tag commit for this release. + """ + tagCommit: Commit + + """ + The name of the release's Git tag + """ + tagName: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this issue + """ + url: URI! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! +} + +""" +A release asset contains the content for a release asset. +""" +type ReleaseAsset implements Node { + """ + The asset's content-type + """ + contentType: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The number of times this asset was downloaded + """ + downloadCount: Int! + + """ + Identifies the URL where you can download the release asset via the browser. + """ + downloadUrl: URI! + id: ID! + + """ + Identifies the title of the release asset. + """ + name: String! + + """ + Release that the asset is associated with + """ + release: Release + + """ + The size (in bytes) of the asset + """ + size: Int! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The user that performed the upload + """ + uploadedBy: User! + + """ + Identifies the URL of the release asset. + """ + url: URI! +} + +""" +The connection type for ReleaseAsset. +""" +type ReleaseAssetConnection { + """ + A list of edges. + """ + edges: [ReleaseAssetEdge] + + """ + A list of nodes. + """ + nodes: [ReleaseAsset] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReleaseAssetEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ReleaseAsset +} + +""" +The connection type for Release. +""" +type ReleaseConnection { + """ + A list of edges. + """ + edges: [ReleaseEdge] + + """ + A list of nodes. + """ + nodes: [Release] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReleaseEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Release +} + +""" +Ways in which lists of releases can be ordered upon return. +""" +input ReleaseOrder { + """ + The direction in which to order releases by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order releases by. + """ + field: ReleaseOrderField! +} + +""" +Properties by which release connections can be ordered. +""" +enum ReleaseOrderField { + """ + Order releases by creation time + """ + CREATED_AT + + """ + Order releases alphabetically by name + """ + NAME +} + +""" +Autogenerated input type of RemoveAssigneesFromAssignable +""" +input RemoveAssigneesFromAssignableInput { + """ + The id of the assignable object to remove assignees from. + """ + assignableId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "Assignable") + + """ + The id of users to remove as assignees. + """ + assigneeIds: [ID!]! @possibleTypes(concreteTypes: ["User"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of RemoveAssigneesFromAssignable +""" +type RemoveAssigneesFromAssignablePayload { + """ + The item that was unassigned. + """ + assignable: Assignable + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of RemoveEnterpriseAdmin +""" +input RemoveEnterpriseAdminInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Enterprise ID from which to remove the administrator. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of the user to remove as an administrator. + """ + login: String! +} + +""" +Autogenerated return type of RemoveEnterpriseAdmin +""" +type RemoveEnterpriseAdminPayload { + """ + The user who was removed as an administrator. + """ + admin: User + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated enterprise. + """ + enterprise: Enterprise + + """ + A message confirming the result of removing an administrator. + """ + message: String + + """ + The viewer performing the mutation. + """ + viewer: User +} + +""" +Autogenerated input type of RemoveEnterpriseIdentityProvider +""" +input RemoveEnterpriseIdentityProviderInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise from which to remove the identity provider. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) +} + +""" +Autogenerated return type of RemoveEnterpriseIdentityProvider +""" +type RemoveEnterpriseIdentityProviderPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The identity provider that was removed from the enterprise. + """ + identityProvider: EnterpriseIdentityProvider +} + +""" +Autogenerated input type of RemoveEnterpriseMember +""" +input RemoveEnterpriseMemberInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise from which the user should be removed. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The ID of the user to remove from the enterprise. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of RemoveEnterpriseMember +""" +type RemoveEnterpriseMemberPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated enterprise. + """ + enterprise: Enterprise + + """ + The user that was removed from the enterprise. + """ + user: User + + """ + The viewer performing the mutation. + """ + viewer: User +} + +""" +Autogenerated input type of RemoveEnterpriseOrganization +""" +input RemoveEnterpriseOrganizationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise from which the organization should be removed. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The ID of the organization to remove from the enterprise. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of RemoveEnterpriseOrganization +""" +type RemoveEnterpriseOrganizationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated enterprise. + """ + enterprise: Enterprise + + """ + The organization that was removed from the enterprise. + """ + organization: Organization + + """ + The viewer performing the mutation. + """ + viewer: User +} + +""" +Autogenerated input type of RemoveEnterpriseSupportEntitlement +""" +input RemoveEnterpriseSupportEntitlementInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Enterprise which the admin belongs to. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a member who will lose the support entitlement. + """ + login: String! +} + +""" +Autogenerated return type of RemoveEnterpriseSupportEntitlement +""" +type RemoveEnterpriseSupportEntitlementPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of removing the support entitlement. + """ + message: String +} + +""" +Autogenerated input type of RemoveLabelsFromLabelable +""" +input RemoveLabelsFromLabelableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ids of labels to remove. + """ + labelIds: [ID!]! @possibleTypes(concreteTypes: ["Label"]) + + """ + The id of the Labelable to remove labels from. + """ + labelableId: ID! @possibleTypes(concreteTypes: ["Discussion", "Issue", "PullRequest"], abstractType: "Labelable") +} + +""" +Autogenerated return type of RemoveLabelsFromLabelable +""" +type RemoveLabelsFromLabelablePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Labelable the labels were removed from. + """ + labelable: Labelable +} + +""" +Autogenerated input type of RemoveOutsideCollaborator +""" +input RemoveOutsideCollaboratorInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the organization to remove the outside collaborator from. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + The ID of the outside collaborator to remove. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of RemoveOutsideCollaborator +""" +type RemoveOutsideCollaboratorPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The user that was removed as an outside collaborator. + """ + removedUser: User +} + +""" +Autogenerated input type of RemoveReaction +""" +input RemoveReactionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the emoji reaction to remove. + """ + content: ReactionContent! + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! + @possibleTypes( + concreteTypes: [ + "CommitComment" + "Discussion" + "DiscussionComment" + "Issue" + "IssueComment" + "PullRequest" + "PullRequestReview" + "PullRequestReviewComment" + "Release" + "TeamDiscussion" + "TeamDiscussionComment" + ] + abstractType: "Reactable" + ) +} + +""" +Autogenerated return type of RemoveReaction +""" +type RemoveReactionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The reaction object. + """ + reaction: Reaction + + """ + The reaction groups for the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + The reactable subject. + """ + subject: Reactable +} + +""" +Autogenerated input type of RemoveStar +""" +input RemoveStarInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Starrable ID to unstar. + """ + starrableId: ID! @possibleTypes(concreteTypes: ["Gist", "Repository", "Topic"], abstractType: "Starrable") +} + +""" +Autogenerated return type of RemoveStar +""" +type RemoveStarPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The starrable. + """ + starrable: Starrable +} + +""" +Autogenerated input type of RemoveUpvote +""" +input RemoveUpvoteInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion or comment to remove upvote. + """ + subjectId: ID! @possibleTypes(concreteTypes: ["Discussion", "DiscussionComment"], abstractType: "Votable") +} + +""" +Autogenerated return type of RemoveUpvote +""" +type RemoveUpvotePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The votable subject. + """ + subject: Votable +} + +""" +Represents a 'removed_from_merge_queue' event on a given pull request. +""" +type RemovedFromMergeQueueEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the before commit SHA for the 'removed_from_merge_queue' event. + """ + beforeCommit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who removed this Pull Request from the merge queue + """ + enqueuer: User + id: ID! + + """ + The merge queue where this pull request was removed from. + """ + mergeQueue: MergeQueue + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest + + """ + The reason this pull request was removed from the queue. + """ + reason: String +} + +""" +Represents a 'removed_from_project' event on a given issue or pull request. +""" +type RemovedFromProjectEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Project referenced by event. + """ + project: Project @preview(toggledBy: "starfox-preview") + + """ + Column name referenced by this project event. + """ + projectColumnName: String! @preview(toggledBy: "starfox-preview") +} + +""" +Represents a 'renamed' event on a given issue or pull request +""" +type RenamedTitleEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the current title of the issue or pull request. + """ + currentTitle: String! + id: ID! + + """ + Identifies the previous title of the issue or pull request. + """ + previousTitle: String! + + """ + Subject that was renamed. + """ + subject: RenamedTitleSubject! +} + +""" +An object which has a renamable title +""" +union RenamedTitleSubject = Issue | PullRequest + +""" +Autogenerated input type of ReopenDiscussion +""" +input ReopenDiscussionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the discussion to be reopened. + """ + discussionId: ID! @possibleTypes(concreteTypes: ["Discussion"]) +} + +""" +Autogenerated return type of ReopenDiscussion +""" +type ReopenDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion that was reopened. + """ + discussion: Discussion +} + +""" +Autogenerated input type of ReopenIssue +""" +input ReopenIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the issue to be opened. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of ReopenIssue +""" +type ReopenIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was opened. + """ + issue: Issue +} + +""" +Autogenerated input type of ReopenPullRequest +""" +input ReopenPullRequestInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to be reopened. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of ReopenPullRequest +""" +type ReopenPullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that was reopened. + """ + pullRequest: PullRequest +} + +""" +Represents a 'reopened' event on any `Closable`. +""" +type ReopenedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Object that was reopened. + """ + closable: Closable! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + The reason the issue state was changed to open. + """ + stateReason: IssueStateReason +} + +""" +Audit log entry for a repo.access event. +""" +type RepoAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The visibility of the repository + """ + visibility: RepoAccessAuditEntryVisibility +} + +""" +The privacy of a repository +""" +enum RepoAccessAuditEntryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.add_member event. +""" +type RepoAddMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The visibility of the repository + """ + visibility: RepoAddMemberAuditEntryVisibility +} + +""" +The privacy of a repository +""" +enum RepoAddMemberAuditEntryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.add_topic event. +""" +type RepoAddTopicAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData & TopicAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The name of the topic added to the repository + """ + topic: Topic + + """ + The name of the topic added to the repository + """ + topicName: String + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.archived event. +""" +type RepoArchivedAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The visibility of the repository + """ + visibility: RepoArchivedAuditEntryVisibility +} + +""" +The privacy of a repository +""" +enum RepoArchivedAuditEntryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.change_merge_setting event. +""" +type RepoChangeMergeSettingAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + Whether the change was to enable (true) or disable (false) the merge type + """ + isEnabled: Boolean + + """ + The merge method affected by the change + """ + mergeType: RepoChangeMergeSettingAuditEntryMergeType + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The merge options available for pull requests to this repository. +""" +enum RepoChangeMergeSettingAuditEntryMergeType { + """ + The pull request is added to the base branch in a merge commit. + """ + MERGE + + """ + Commits from the pull request are added onto the base branch individually without a merge commit. + """ + REBASE + + """ + The pull request's commits are squashed into a single commit before they are merged to the base branch. + """ + SQUASH +} + +""" +Audit log entry for a repo.config.disable_anonymous_git_access event. +""" +type RepoConfigDisableAnonymousGitAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.disable_collaborators_only event. +""" +type RepoConfigDisableCollaboratorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.disable_contributors_only event. +""" +type RepoConfigDisableContributorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.disable_sockpuppet_disallowed event. +""" +type RepoConfigDisableSockpuppetDisallowedAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.enable_anonymous_git_access event. +""" +type RepoConfigEnableAnonymousGitAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.enable_collaborators_only event. +""" +type RepoConfigEnableCollaboratorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.enable_contributors_only event. +""" +type RepoConfigEnableContributorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.enable_sockpuppet_disallowed event. +""" +type RepoConfigEnableSockpuppetDisallowedAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.lock_anonymous_git_access event. +""" +type RepoConfigLockAnonymousGitAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.unlock_anonymous_git_access event. +""" +type RepoConfigUnlockAnonymousGitAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.create event. +""" +type RepoCreateAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The name of the parent repository for this forked repository. + """ + forkParentName: String + + """ + The name of the root repository for this network. + """ + forkSourceName: String + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The visibility of the repository + """ + visibility: RepoCreateAuditEntryVisibility +} + +""" +The privacy of a repository +""" +enum RepoCreateAuditEntryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.destroy event. +""" +type RepoDestroyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The visibility of the repository + """ + visibility: RepoDestroyAuditEntryVisibility +} + +""" +The privacy of a repository +""" +enum RepoDestroyAuditEntryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.remove_member event. +""" +type RepoRemoveMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The visibility of the repository + """ + visibility: RepoRemoveMemberAuditEntryVisibility +} + +""" +The privacy of a repository +""" +enum RepoRemoveMemberAuditEntryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.remove_topic event. +""" +type RepoRemoveTopicAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData & TopicAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The name of the topic added to the repository + """ + topic: Topic + + """ + The name of the topic added to the repository + """ + topicName: String + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The reasons a piece of content can be reported or minimized. +""" +enum ReportedContentClassifiers { + """ + An abusive or harassing piece of content + """ + ABUSE + + """ + A duplicated piece of content + """ + DUPLICATE + + """ + An irrelevant piece of content + """ + OFF_TOPIC + + """ + An outdated piece of content + """ + OUTDATED + + """ + The content has been resolved + """ + RESOLVED + + """ + A spammy piece of content + """ + SPAM +} + +""" +A repository contains the content for a project. +""" +type Repository implements Node & PackageOwner & ProjectOwner & ProjectV2Recent & RepositoryInfo & Starrable & Subscribable & UniformResourceLocatable { + """ + Whether or not a pull request head branch that is behind its base branch can + always be updated even if it is not required to be up to date before merging. + """ + allowUpdateBranch: Boolean! + + """ + Identifies the date and time when the repository was archived. + """ + archivedAt: DateTime + + """ + A list of users that can be assigned to issues in this repository. + """ + assignableUsers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filters users with query on user name and login. + """ + query: String + ): UserConnection! + + """ + Whether or not Auto-merge can be enabled on pull requests in this repository. + """ + autoMergeAllowed: Boolean! + + """ + A list of branch protection rules for this repository. + """ + branchProtectionRules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): BranchProtectionRuleConnection! + + """ + Returns the code of conduct for this repository + """ + codeOfConduct: CodeOfConduct + + """ + Information extracted from the repository's `CODEOWNERS` file. + """ + codeowners( + """ + The ref name used to return the associated `CODEOWNERS` file. + """ + refName: String + ): RepositoryCodeowners + + """ + A list of collaborators associated with the repository. + """ + collaborators( + """ + Collaborators affiliation level with a repository. + """ + affiliation: CollaboratorAffiliation + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The login of one specific collaborator. + """ + login: String + + """ + Filters users with query on user name and login + """ + query: String + ): RepositoryCollaboratorConnection + + """ + A list of commit comments associated with the repository. + """ + commitComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + Returns a list of contact links associated to the repository + """ + contactLinks: [RepositoryContactLink!] + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Ref associated with the repository's default branch. + """ + defaultBranchRef: Ref + + """ + Whether or not branches are automatically deleted when merged in this repository. + """ + deleteBranchOnMerge: Boolean! + + """ + A list of dependency manifests contained in the repository + """ + dependencyGraphManifests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Cursor to paginate dependencies + """ + dependenciesAfter: String + + """ + Number of dependencies to fetch + """ + dependenciesFirst: Int + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Flag to scope to only manifests with dependencies + """ + withDependencies: Boolean + ): DependencyGraphManifestConnection @preview(toggledBy: "hawkgirl-preview") + + """ + A list of deploy keys that are on this repository. + """ + deployKeys( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeployKeyConnection! + + """ + Deployments associated with the repository + """ + deployments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Environments to list deployments for + """ + environments: [String!] + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for deployments returned from the connection. + """ + orderBy: DeploymentOrder = {field: CREATED_AT, direction: ASC} + ): DeploymentConnection! + + """ + The description of the repository. + """ + description: String + + """ + The description of the repository rendered to HTML. + """ + descriptionHTML: HTML! + + """ + Returns a single discussion from the current repository by number. + """ + discussion( + """ + The number for the discussion to be returned. + """ + number: Int! + ): Discussion + + """ + A list of discussion categories that are available in the repository. + """ + discussionCategories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filter by categories that are assignable by the viewer. + """ + filterByAssignable: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DiscussionCategoryConnection! + + """ + A discussion category by slug. + """ + discussionCategory( + """ + The slug of the discussion category to be returned. + """ + slug: String! + ): DiscussionCategory + + """ + A list of discussions that have been opened in the repository. + """ + discussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Only include discussions that belong to the category with this ID. + """ + categoryId: ID = null + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for discussions returned from the connection. + """ + orderBy: DiscussionOrder = {field: UPDATED_AT, direction: DESC} + + """ + A list of states to filter the discussions by. + """ + states: [DiscussionState!] = [] + ): DiscussionConnection! + + """ + The number of kilobytes this repository occupies on disk. + """ + diskUsage: Int + + """ + Returns a single active environment from the current repository by name. + """ + environment( + """ + The name of the environment to be returned. + """ + name: String! + ): Environment + + """ + A list of environments that are in this repository. + """ + environments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the environments + """ + orderBy: Environments = {field: NAME, direction: ASC} + ): EnvironmentConnection! + + """ + Returns how many forks there are of this repository in the whole network. + """ + forkCount: Int! + + """ + Whether this repository allows forks. + """ + forkingAllowed: Boolean! + + """ + A list of direct forked repositories. + """ + forks( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssuesEnabled: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + The funding links for this repository + """ + fundingLinks: [FundingLink!]! + + """ + Indicates if the repository has the Discussions feature enabled. + """ + hasDiscussionsEnabled: Boolean! + + """ + Indicates if the repository has issues feature enabled. + """ + hasIssuesEnabled: Boolean! + + """ + Indicates if the repository has the Projects feature enabled. + """ + hasProjectsEnabled: Boolean! + + """ + Whether vulnerability alerts are enabled for the repository. + """ + hasVulnerabilityAlertsEnabled: Boolean! + + """ + Indicates if the repository has wiki feature enabled. + """ + hasWikiEnabled: Boolean! + + """ + The repository's URL. + """ + homepageUrl: URI + id: ID! + + """ + The interaction ability settings for this repository. + """ + interactionAbility: RepositoryInteractionAbility + + """ + Indicates if the repository is unmaintained. + """ + isArchived: Boolean! + + """ + Returns true if blank issue creation is allowed + """ + isBlankIssuesEnabled: Boolean! + + """ + Returns whether or not this repository disabled. + """ + isDisabled: Boolean! + + """ + Returns whether or not this repository is empty. + """ + isEmpty: Boolean! + + """ + Identifies if the repository is a fork. + """ + isFork: Boolean! + + """ + Indicates if a repository is either owned by an organization, or is a private fork of an organization repository. + """ + isInOrganization: Boolean! + + """ + Indicates if the repository has been locked or not. + """ + isLocked: Boolean! + + """ + Identifies if the repository is a mirror. + """ + isMirror: Boolean! + + """ + Identifies if the repository is private or internal. + """ + isPrivate: Boolean! + + """ + Returns true if this repository has a security policy + """ + isSecurityPolicyEnabled: Boolean + + """ + Identifies if the repository is a template that can be used to generate new repositories. + """ + isTemplate: Boolean! + + """ + Is this repository a user configuration repository? + """ + isUserConfigurationRepository: Boolean! + + """ + Returns a single issue from the current repository by number. + """ + issue( + """ + The number for the issue to be returned. + """ + number: Int! + ): Issue + + """ + Returns a single issue-like object from the current repository by number. + """ + issueOrPullRequest( + """ + The number for the issue to be returned. + """ + number: Int! + ): IssueOrPullRequest + + """ + Returns a list of issue templates associated to the repository + """ + issueTemplates: [IssueTemplate!] + + """ + A list of issues that have been opened in the repository. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + Returns a single label by name + """ + label( + """ + Label name + """ + name: String! + ): Label + + """ + A list of labels associated with the repository. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + + """ + If provided, searches labels by name and description. + """ + query: String + ): LabelConnection + + """ + A list containing a breakdown of the language composition of the repository. + """ + languages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: LanguageOrder + ): LanguageConnection + + """ + Get the latest release for the repository if one exists. + """ + latestRelease: Release + + """ + The license associated with the repository + """ + licenseInfo: License + + """ + The reason the repository has been locked. + """ + lockReason: RepositoryLockReason + + """ + A list of Users that can be mentioned in the context of the repository. + """ + mentionableUsers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filters users with query on user name and login + """ + query: String + ): UserConnection! + + """ + Whether or not PRs are merged with a merge commit on this repository. + """ + mergeCommitAllowed: Boolean! + + """ + How the default commit message will be generated when merging a pull request. + """ + mergeCommitMessage: MergeCommitMessage! + + """ + How the default commit title will be generated when merging a pull request. + """ + mergeCommitTitle: MergeCommitTitle! + + """ + The merge queue for a specified branch, otherwise the default branch if not provided. + """ + mergeQueue( + """ + The name of the branch to get the merge queue for. Case sensitive. + """ + branch: String + ): MergeQueue + + """ + Returns a single milestone from the current repository by number. + """ + milestone( + """ + The number for the milestone to be returned. + """ + number: Int! + ): Milestone + + """ + A list of milestones associated with the repository. + """ + milestones( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for milestones. + """ + orderBy: MilestoneOrder + + """ + Filters milestones with a query on the title + """ + query: String + + """ + Filter by the state of the milestones. + """ + states: [MilestoneState!] + ): MilestoneConnection + + """ + The repository's original mirror URL. + """ + mirrorUrl: URI + + """ + The name of the repository. + """ + name: String! + + """ + The repository's name with owner. + """ + nameWithOwner: String! + + """ + A Git object in the repository + """ + object( + """ + A Git revision expression suitable for rev-parse + """ + expression: String + + """ + The Git object ID + """ + oid: GitObjectID + ): GitObject + + """ + The image used to represent this repository in Open Graph data. + """ + openGraphImageUrl: URI! + + """ + The User owner of the repository. + """ + owner: RepositoryOwner! + + """ + A list of packages under the owner. + """ + packages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Find packages by their names. + """ + names: [String] + + """ + Ordering of the returned packages. + """ + orderBy: PackageOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter registry package by type. + """ + packageType: PackageType + + """ + Find packages in a repository by ID. + """ + repositoryId: ID + ): PackageConnection! + + """ + The repository parent, if this is a fork. + """ + parent: Repository + + """ + A list of discussions that have been pinned in this repository. + """ + pinnedDiscussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PinnedDiscussionConnection! + + """ + A list of pinned issues for this repository. + """ + pinnedIssues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PinnedIssueConnection + + """ + The primary language of the repository's code. + """ + primaryLanguage: Language + + """ + Find project by number. + """ + project( + """ + The project number to find. + """ + number: Int! + ): Project + + """ + Finds and returns the Project according to the provided Project number. + """ + projectV2( + """ + The Project number. + """ + number: Int! + ): ProjectV2 + + """ + A list of projects under the owner. + """ + projects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for projects returned from the connection + """ + orderBy: ProjectOrder + + """ + Query to search projects by, currently only searching by name. + """ + search: String + + """ + A list of states to filter the projects by. + """ + states: [ProjectState!] + ): ProjectConnection! + + """ + The HTTP path listing the repository's projects + """ + projectsResourcePath: URI! + + """ + The HTTP URL listing the repository's projects + """ + projectsUrl: URI! + + """ + List of projects linked to this repository. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + A project to search for linked to the repo. + """ + query: String + ): ProjectV2Connection! + + """ + Returns a single pull request from the current repository by number. + """ + pullRequest( + """ + The number for the pull request to be returned. + """ + number: Int! + ): PullRequest + + """ + Returns a list of pull request templates associated to the repository + """ + pullRequestTemplates: [PullRequestTemplate!] + + """ + A list of pull requests that have been opened in the repository. + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + Identifies the date and time when the repository was last pushed to. + """ + pushedAt: DateTime + + """ + Whether or not rebase-merging is enabled on this repository. + """ + rebaseMergeAllowed: Boolean! + + """ + Recent projects that this user has modified in the context of the owner. + """ + recentProjects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2Connection! + + """ + Fetch a given ref from the repository + """ + ref( + """ + The ref to retrieve. Fully qualified matches are checked in order + (`refs/heads/master`) before falling back onto checks for short name matches (`master`). + """ + qualifiedName: String! + ): Ref + + """ + Fetch a list of refs from the repository + """ + refs( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + DEPRECATED: use orderBy. The ordering direction. + """ + direction: OrderDirection + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for refs returned from the connection. + """ + orderBy: RefOrder + + """ + Filters refs with query on name + """ + query: String + + """ + A ref name prefix like `refs/heads/`, `refs/tags/`, etc. + """ + refPrefix: String! + ): RefConnection + + """ + Lookup a single release given various criteria. + """ + release( + """ + The name of the Tag the Release was created from + """ + tagName: String! + ): Release + + """ + List of releases which are dependent on this repository. + """ + releases( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: ReleaseOrder + ): ReleaseConnection! + + """ + A list of applied repository-topic associations for this repository. + """ + repositoryTopics( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RepositoryTopicConnection! + + """ + The HTTP path for this repository + """ + resourcePath: URI! + + """ + Returns a single ruleset from the current repository by ID. + """ + ruleset( + """ + The ID of the ruleset to be returned. + """ + databaseId: Int! + + """ + Include rulesets configured at higher levels that apply to this repository + """ + includeParents: Boolean = true + ): RepositoryRuleset + + """ + A list of rulesets for this repository. + """ + rulesets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Return rulesets configured at higher levels that apply to this repository + """ + includeParents: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RepositoryRulesetConnection + + """ + The security policy URL. + """ + securityPolicyUrl: URI + + """ + A description of the repository, rendered to HTML without any links in it. + """ + shortDescriptionHTML( + """ + How many characters to return. + """ + limit: Int = 200 + ): HTML! + + """ + Whether or not squash-merging is enabled on this repository. + """ + squashMergeAllowed: Boolean! + + """ + How the default commit message will be generated when squash merging a pull request. + """ + squashMergeCommitMessage: SquashMergeCommitMessage! + + """ + How the default commit title will be generated when squash merging a pull request. + """ + squashMergeCommitTitle: SquashMergeCommitTitle! + + """ + Whether a squash merge commit can use the pull request title as default. + """ + squashPrTitleUsedAsDefault: Boolean! + @deprecated( + reason: "`squashPrTitleUsedAsDefault` will be removed. Use `Repository.squashMergeCommitTitle` instead. Removal on 2023-04-01 UTC." + ) + + """ + The SSH URL to clone this repository + """ + sshUrl: GitSSHRemote! + + """ + Returns a count of how many stargazers there are on this object + """ + stargazerCount: Int! + + """ + A list of users who have starred this starrable. + """ + stargazers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + ): StargazerConnection! + + """ + Returns a list of all submodules in this repository parsed from the + .gitmodules file as of the default branch's HEAD commit. + """ + submodules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): SubmoduleConnection! + + """ + Temporary authentication token for cloning this repository. + """ + tempCloneToken: String + + """ + The repository from which this repository was generated, if any. + """ + templateRepository: Repository + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this repository + """ + url: URI! + + """ + Whether this repository has a custom image to use with Open Graph as opposed to being represented by the owner's avatar. + """ + usesCustomOpenGraphImage: Boolean! + + """ + Indicates whether the viewer has admin permissions on this repository. + """ + viewerCanAdminister: Boolean! + + """ + Can the current viewer create new projects on this owner. + """ + viewerCanCreateProjects: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Indicates whether the viewer can update the topics of this repository. + """ + viewerCanUpdateTopics: Boolean! + + """ + The last commit email for the viewer. + """ + viewerDefaultCommitEmail: String + + """ + The last used merge method by the viewer or the default for the repository. + """ + viewerDefaultMergeMethod: PullRequestMergeMethod! + + """ + Returns a boolean indicating whether the viewing user has starred this starrable. + """ + viewerHasStarred: Boolean! + + """ + The users permission level on the repository. Will return null if authenticated as an GitHub App. + """ + viewerPermission: RepositoryPermission + + """ + A list of emails this viewer can commit with. + """ + viewerPossibleCommitEmails: [String!] + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState + + """ + Indicates the repository's visibility level. + """ + visibility: RepositoryVisibility! + + """ + Returns a single vulnerability alert from the current repository by number. + """ + vulnerabilityAlert( + """ + The number for the vulnerability alert to be returned. + """ + number: Int! + ): RepositoryVulnerabilityAlert + + """ + A list of vulnerability alerts that are on this repository. + """ + vulnerabilityAlerts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filter by the scope of the alert's dependency + """ + dependencyScopes: [RepositoryVulnerabilityAlertDependencyScope!] + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter by the state of the alert + """ + states: [RepositoryVulnerabilityAlertState!] + ): RepositoryVulnerabilityAlertConnection + + """ + A list of users watching the repository. + """ + watchers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + Whether contributors are required to sign off on web-based commits in this repository. + """ + webCommitSignoffRequired: Boolean! +} + +""" +The affiliation of a user to a repository +""" +enum RepositoryAffiliation { + """ + Repositories that the user has been added to as a collaborator. + """ + COLLABORATOR + + """ + Repositories that the user has access to through being a member of an + organization. This includes every repository on every team that the user is on. + """ + ORGANIZATION_MEMBER + + """ + Repositories that are owned by the authenticated user. + """ + OWNER +} + +""" +Metadata for an audit entry with action repo.* +""" +interface RepositoryAuditEntryData { + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI +} + +""" +Information extracted from a repository's `CODEOWNERS` file. +""" +type RepositoryCodeowners { + """ + Any problems that were encountered while parsing the `CODEOWNERS` file. + """ + errors: [RepositoryCodeownersError!]! +} + +""" +An error in a `CODEOWNERS` file. +""" +type RepositoryCodeownersError { + """ + The column number where the error occurs. + """ + column: Int! + + """ + A short string describing the type of error. + """ + kind: String! + + """ + The line number where the error occurs. + """ + line: Int! + + """ + A complete description of the error, combining information from other fields. + """ + message: String! + + """ + The path to the file when the error occurs. + """ + path: String! + + """ + The content of the line where the error occurs. + """ + source: String! + + """ + A suggestion of how to fix the error. + """ + suggestion: String +} + +""" +The connection type for User. +""" +type RepositoryCollaboratorConnection { + """ + A list of edges. + """ + edges: [RepositoryCollaboratorEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user who is a collaborator of a repository. +""" +type RepositoryCollaboratorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: User! + + """ + The permission the user has on the repository. + """ + permission: RepositoryPermission! + + """ + A list of sources for the user's access to the repository. + """ + permissionSources: [PermissionSource!] +} + +""" +A list of repositories owned by the subject. +""" +type RepositoryConnection { + """ + A list of edges. + """ + edges: [RepositoryEdge] + + """ + A list of nodes. + """ + nodes: [Repository] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + The total size in kilobytes of all repositories in the connection. + """ + totalDiskUsage: Int! +} + +""" +A repository contact link. +""" +type RepositoryContactLink { + """ + The contact link purpose. + """ + about: String! + + """ + The contact link name. + """ + name: String! + + """ + The contact link URL. + """ + url: URI! +} + +""" +The reason a repository is listed as 'contributed'. +""" +enum RepositoryContributionType { + """ + Created a commit + """ + COMMIT + + """ + Created an issue + """ + ISSUE + + """ + Created a pull request + """ + PULL_REQUEST + + """ + Reviewed a pull request + """ + PULL_REQUEST_REVIEW + + """ + Created the repository + """ + REPOSITORY +} + +""" +Represents an author of discussions in repositories. +""" +interface RepositoryDiscussionAuthor { + """ + Discussions this user has started. + """ + repositoryDiscussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Filter discussions to only those that have been answered or not. Defaults to + including both answered and unanswered discussions. + """ + answered: Boolean = null + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for discussions returned from the connection. + """ + orderBy: DiscussionOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter discussions to only those in a specific repository. + """ + repositoryId: ID + + """ + A list of states to filter the discussions by. + """ + states: [DiscussionState!] = [] + ): DiscussionConnection! +} + +""" +Represents an author of discussion comments in repositories. +""" +interface RepositoryDiscussionCommentAuthor { + """ + Discussion comments this user has authored. + """ + repositoryDiscussionComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter discussion comments to only those that were marked as the answer + """ + onlyAnswers: Boolean = false + + """ + Filter discussion comments to only those in a specific repository. + """ + repositoryId: ID + ): DiscussionCommentConnection! +} + +""" +An edge in a connection. +""" +type RepositoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Repository +} + +""" +Parameters to be used for the repository_id condition +""" +type RepositoryIdConditionTarget { + """ + One of these repo IDs must match the repo. + """ + repositoryIds: [ID!]! +} + +""" +Parameters to be used for the repository_id condition +""" +input RepositoryIdConditionTargetInput { + """ + One of these repo IDs must match the repo. + """ + repositoryIds: [ID!]! +} + +""" +A subset of repository info. +""" +interface RepositoryInfo { + """ + Identifies the date and time when the repository was archived. + """ + archivedAt: DateTime + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The description of the repository. + """ + description: String + + """ + The description of the repository rendered to HTML. + """ + descriptionHTML: HTML! + + """ + Returns how many forks there are of this repository in the whole network. + """ + forkCount: Int! + + """ + Indicates if the repository has the Discussions feature enabled. + """ + hasDiscussionsEnabled: Boolean! + + """ + Indicates if the repository has issues feature enabled. + """ + hasIssuesEnabled: Boolean! + + """ + Indicates if the repository has the Projects feature enabled. + """ + hasProjectsEnabled: Boolean! + + """ + Indicates if the repository has wiki feature enabled. + """ + hasWikiEnabled: Boolean! + + """ + The repository's URL. + """ + homepageUrl: URI + + """ + Indicates if the repository is unmaintained. + """ + isArchived: Boolean! + + """ + Identifies if the repository is a fork. + """ + isFork: Boolean! + + """ + Indicates if a repository is either owned by an organization, or is a private fork of an organization repository. + """ + isInOrganization: Boolean! + + """ + Indicates if the repository has been locked or not. + """ + isLocked: Boolean! + + """ + Identifies if the repository is a mirror. + """ + isMirror: Boolean! + + """ + Identifies if the repository is private or internal. + """ + isPrivate: Boolean! + + """ + Identifies if the repository is a template that can be used to generate new repositories. + """ + isTemplate: Boolean! + + """ + The license associated with the repository + """ + licenseInfo: License + + """ + The reason the repository has been locked. + """ + lockReason: RepositoryLockReason + + """ + The repository's original mirror URL. + """ + mirrorUrl: URI + + """ + The name of the repository. + """ + name: String! + + """ + The repository's name with owner. + """ + nameWithOwner: String! + + """ + The image used to represent this repository in Open Graph data. + """ + openGraphImageUrl: URI! + + """ + The User owner of the repository. + """ + owner: RepositoryOwner! + + """ + Identifies the date and time when the repository was last pushed to. + """ + pushedAt: DateTime + + """ + The HTTP path for this repository + """ + resourcePath: URI! + + """ + A description of the repository, rendered to HTML without any links in it. + """ + shortDescriptionHTML( + """ + How many characters to return. + """ + limit: Int = 200 + ): HTML! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this repository + """ + url: URI! + + """ + Whether this repository has a custom image to use with Open Graph as opposed to being represented by the owner's avatar. + """ + usesCustomOpenGraphImage: Boolean! + + """ + Indicates the repository's visibility level. + """ + visibility: RepositoryVisibility! +} + +""" +Repository interaction limit that applies to this object. +""" +type RepositoryInteractionAbility { + """ + The time the currently active limit expires. + """ + expiresAt: DateTime + + """ + The current limit that is enabled on this object. + """ + limit: RepositoryInteractionLimit! + + """ + The origin of the currently active interaction limit. + """ + origin: RepositoryInteractionLimitOrigin! +} + +""" +A repository interaction limit. +""" +enum RepositoryInteractionLimit { + """ + Users that are not collaborators will not be able to interact with the repository. + """ + COLLABORATORS_ONLY + + """ + Users that have not previously committed to a repository’s default branch will be unable to interact with the repository. + """ + CONTRIBUTORS_ONLY + + """ + Users that have recently created their account will be unable to interact with the repository. + """ + EXISTING_USERS + + """ + No interaction limits are enabled. + """ + NO_LIMIT +} + +""" +The length for a repository interaction limit to be enabled for. +""" +enum RepositoryInteractionLimitExpiry { + """ + The interaction limit will expire after 1 day. + """ + ONE_DAY + + """ + The interaction limit will expire after 1 month. + """ + ONE_MONTH + + """ + The interaction limit will expire after 1 week. + """ + ONE_WEEK + + """ + The interaction limit will expire after 6 months. + """ + SIX_MONTHS + + """ + The interaction limit will expire after 3 days. + """ + THREE_DAYS +} + +""" +Indicates where an interaction limit is configured. +""" +enum RepositoryInteractionLimitOrigin { + """ + A limit that is configured at the organization level. + """ + ORGANIZATION + + """ + A limit that is configured at the repository level. + """ + REPOSITORY + + """ + A limit that is configured at the user-wide level. + """ + USER +} + +""" +An invitation for a user to be added to a repository. +""" +type RepositoryInvitation implements Node { + """ + The email address that received the invitation. + """ + email: String + id: ID! + + """ + The user who received the invitation. + """ + invitee: User + + """ + The user who created the invitation. + """ + inviter: User! + + """ + The permalink for this repository invitation. + """ + permalink: URI! + + """ + The permission granted on this repository by this invitation. + """ + permission: RepositoryPermission! + + """ + The Repository the user is invited to. + """ + repository: RepositoryInfo +} + +""" +A list of repository invitations. +""" +type RepositoryInvitationConnection { + """ + A list of edges. + """ + edges: [RepositoryInvitationEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryInvitation +} + +""" +Ordering options for repository invitation connections. +""" +input RepositoryInvitationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repository invitations by. + """ + field: RepositoryInvitationOrderField! +} + +""" +Properties by which repository invitation connections can be ordered. +""" +enum RepositoryInvitationOrderField { + """ + Order repository invitations by creation time + """ + CREATED_AT +} + +""" +The possible reasons a given repository could be in a locked state. +""" +enum RepositoryLockReason { + """ + The repository is locked due to a billing related reason. + """ + BILLING + + """ + The repository is locked due to a migration. + """ + MIGRATING + + """ + The repository is locked due to a move. + """ + MOVING + + """ + The repository is locked due to a rename. + """ + RENAME + + """ + The repository is locked due to a trade controls related reason. + """ + TRADE_RESTRICTION +} + +""" +A GitHub Enterprise Importer (GEI) repository migration. +""" +type RepositoryMigration implements Migration & Node { + """ + The migration flag to continue on error. + """ + continueOnError: Boolean! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: String + + """ + The reason the migration failed. + """ + failureReason: String + id: ID! + + """ + The URL for the migration log (expires 1 day after migration completes). + """ + migrationLogUrl: URI + + """ + The migration source. + """ + migrationSource: MigrationSource! + + """ + The target repository name. + """ + repositoryName: String! + + """ + The migration source URL, for example `https://github.com` or `https://monalisa.ghe.com`. + """ + sourceUrl: URI! + + """ + The migration state. + """ + state: MigrationState! + + """ + The number of warnings encountered for this migration. To review the warnings, + check the [Migration Log](https://docs.github.com/en/migrations/using-github-enterprise-importer/completing-your-migration-with-github-enterprise-importer/accessing-your-migration-logs-for-github-enterprise-importer). + """ + warningsCount: Int! +} + +""" +The connection type for RepositoryMigration. +""" +type RepositoryMigrationConnection { + """ + A list of edges. + """ + edges: [RepositoryMigrationEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryMigration] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a repository migration. +""" +type RepositoryMigrationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryMigration +} + +""" +Ordering options for repository migrations. +""" +input RepositoryMigrationOrder { + """ + The ordering direction. + """ + direction: RepositoryMigrationOrderDirection! + + """ + The field to order repository migrations by. + """ + field: RepositoryMigrationOrderField! +} + +""" +Possible directions in which to order a list of repository migrations when provided an `orderBy` argument. +""" +enum RepositoryMigrationOrderDirection { + """ + Specifies an ascending order for a given `orderBy` argument. + """ + ASC + + """ + Specifies a descending order for a given `orderBy` argument. + """ + DESC +} + +""" +Properties by which repository migrations can be ordered. +""" +enum RepositoryMigrationOrderField { + """ + Order mannequins why when they were created. + """ + CREATED_AT +} + +""" +Parameters to be used for the repository_name condition +""" +type RepositoryNameConditionTarget { + """ + Array of repository names or patterns to exclude. The condition will not pass if any of these patterns match. + """ + exclude: [String!]! + + """ + Array of repository names or patterns to include. One of these patterns must + match for the condition to pass. Also accepts `~ALL` to include all repositories. + """ + include: [String!]! + + """ + Target changes that match these patterns will be prevented except by those with bypass permissions. + """ + protected: Boolean! +} + +""" +Parameters to be used for the repository_name condition +""" +input RepositoryNameConditionTargetInput { + """ + Array of repository names or patterns to exclude. The condition will not pass if any of these patterns match. + """ + exclude: [String!]! + + """ + Array of repository names or patterns to include. One of these patterns must + match for the condition to pass. Also accepts `~ALL` to include all repositories. + """ + include: [String!]! + + """ + Target changes that match these patterns will be prevented except by those with bypass permissions. + """ + protected: Boolean +} + +""" +Represents a object that belongs to a repository. +""" +interface RepositoryNode { + """ + The repository associated with this node. + """ + repository: Repository! +} + +""" +Ordering options for repository connections +""" +input RepositoryOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repositories by. + """ + field: RepositoryOrderField! +} + +""" +Properties by which repository connections can be ordered. +""" +enum RepositoryOrderField { + """ + Order repositories by creation time + """ + CREATED_AT + + """ + Order repositories by name + """ + NAME + + """ + Order repositories by push time + """ + PUSHED_AT + + """ + Order repositories by number of stargazers + """ + STARGAZERS + + """ + Order repositories by update time + """ + UPDATED_AT +} + +""" +Represents an owner of a Repository. +""" +interface RepositoryOwner { + """ + A URL pointing to the owner's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + id: ID! + + """ + The username used to login. + """ + login: String! + + """ + A list of repositories that the user owns. + """ + repositories( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssuesEnabled: Boolean + + """ + If non-null, filters repositories according to whether they are archived and not maintained + """ + isArchived: Boolean + + """ + If non-null, filters repositories according to whether they are forks of another repository + """ + isFork: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + Find Repository. + """ + repository( + """ + Follow repository renames. If disabled, a repository referenced by its old name will return an error. + """ + followRenames: Boolean = true + + """ + Name of Repository to find. + """ + name: String! + ): Repository + + """ + The HTTP URL for the owner. + """ + resourcePath: URI! + + """ + The HTTP URL for the owner. + """ + url: URI! +} + +""" +The access level to a repository +""" +enum RepositoryPermission { + """ + Can read, clone, and push to this repository. Can also manage issues, pull + requests, and repository settings, including adding collaborators + """ + ADMIN + + """ + Can read, clone, and push to this repository. They can also manage issues, pull requests, and some repository settings + """ + MAINTAIN + + """ + Can read and clone this repository. Can also open and comment on issues and pull requests + """ + READ + + """ + Can read and clone this repository. Can also manage issues and pull requests + """ + TRIAGE + + """ + Can read, clone, and push to this repository. Can also manage issues and pull requests + """ + WRITE +} + +""" +The privacy of a repository +""" +enum RepositoryPrivacy { + """ + Private + """ + PRIVATE + + """ + Public + """ + PUBLIC +} + +""" +A repository rule. +""" +type RepositoryRule implements Node { + id: ID! + + """ + The parameters for this rule. + """ + parameters: RuleParameters + + """ + The type of rule. + """ + type: RepositoryRuleType! +} + +""" +Set of conditions that determine if a ruleset will evaluate +""" +type RepositoryRuleConditions { + """ + Configuration for the ref_name condition + """ + refName: RefNameConditionTarget + + """ + Configuration for the repository_id condition + """ + repositoryId: RepositoryIdConditionTarget + + """ + Configuration for the repository_name condition + """ + repositoryName: RepositoryNameConditionTarget +} + +""" +Specifies the conditions required for a ruleset to evaluate +""" +input RepositoryRuleConditionsInput { + """ + Configuration for the ref_name condition + """ + refName: RefNameConditionTargetInput + + """ + Configuration for the repository_id condition + """ + repositoryId: RepositoryIdConditionTargetInput + + """ + Configuration for the repository_name condition + """ + repositoryName: RepositoryNameConditionTargetInput +} + +""" +The connection type for RepositoryRule. +""" +type RepositoryRuleConnection { + """ + A list of edges. + """ + edges: [RepositoryRuleEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryRule] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryRuleEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryRule +} + +""" +Specifies the attributes for a new or updated rule. +""" +input RepositoryRuleInput { + """ + Optional ID of this rule when updating + """ + id: ID @possibleTypes(concreteTypes: ["RepositoryRule"]) + + """ + The parameters for the rule. + """ + parameters: RuleParametersInput + + """ + The type of rule to create. + """ + type: RepositoryRuleType! +} + +""" +The rule types supported in rulesets +""" +enum RepositoryRuleType { + """ + Branch name pattern + """ + BRANCH_NAME_PATTERN + + """ + Committer email pattern + """ + COMMITTER_EMAIL_PATTERN + + """ + Commit author email pattern + """ + COMMIT_AUTHOR_EMAIL_PATTERN + + """ + Commit message pattern + """ + COMMIT_MESSAGE_PATTERN + + """ + Only allow users with bypass permission to create matching refs. + """ + CREATION + + """ + Only allow users with bypass permissions to delete matching refs. + """ + DELETION + + """ + Prevent users with push access from force pushing to branches. + """ + NON_FAST_FORWARD + + """ + Require all commits be made to a non-target branch and submitted via a pull request before they can be merged. + """ + PULL_REQUEST + + """ + Choose which environments must be successfully deployed to before branches can + be merged into a branch that matches this rule. + """ + REQUIRED_DEPLOYMENTS + + """ + Prevent merge commits from being pushed to matching branches. + """ + REQUIRED_LINEAR_HISTORY + + """ + Commits pushed to matching branches must have verified signatures. + """ + REQUIRED_SIGNATURES + + """ + Choose which status checks must pass before branches can be merged into a + branch that matches this rule. When enabled, commits must first be pushed to + another branch, then merged or pushed directly to a branch that matches this + rule after status checks have passed. + """ + REQUIRED_STATUS_CHECKS + + """ + Tag name pattern + """ + TAG_NAME_PATTERN + + """ + Only allow users with bypass permission to update matching refs. + """ + UPDATE +} + +""" +A repository ruleset. +""" +type RepositoryRuleset implements Node { + """ + The actors that can bypass this ruleset + """ + bypassActors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RepositoryRulesetBypassActorConnection + + """ + The set of conditions that must evaluate to true for this ruleset to apply + """ + conditions: RepositoryRuleConditions! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The enforcement level of this ruleset + """ + enforcement: RuleEnforcement! + id: ID! + + """ + Name of the ruleset. + """ + name: String! + + """ + List of rules. + """ + rules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The type of rule. + """ + type: RepositoryRuleType + ): RepositoryRuleConnection + + """ + Source of ruleset. + """ + source: RuleSource! + + """ + Target of the ruleset. + """ + target: RepositoryRulesetTarget + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +A team or app that has the ability to bypass a rules defined on a ruleset +""" +type RepositoryRulesetBypassActor implements Node { + """ + The actor that can bypass rules. + """ + actor: BypassActor + + """ + The mode for the bypass actor + """ + bypassMode: RepositoryRulesetBypassActorBypassMode + id: ID! + + """ + This actor represents the ability for an organization admin to bypass + """ + organizationAdmin: Boolean! + + """ + If the actor is a repository role, the repository role's ID that can bypass + """ + repositoryRoleDatabaseId: Int + + """ + If the actor is a repository role, the repository role's name that can bypass + """ + repositoryRoleName: String + + """ + Identifies the ruleset associated with the allowed actor + """ + repositoryRuleset: RepositoryRuleset +} + +""" +The bypass mode for a specific actor on a ruleset. +""" +enum RepositoryRulesetBypassActorBypassMode { + """ + The actor can always bypass rules + """ + ALWAYS + + """ + The actor can only bypass rules via a pull request + """ + PULL_REQUEST +} + +""" +The connection type for RepositoryRulesetBypassActor. +""" +type RepositoryRulesetBypassActorConnection { + """ + A list of edges. + """ + edges: [RepositoryRulesetBypassActorEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryRulesetBypassActor] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryRulesetBypassActorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryRulesetBypassActor +} + +""" +Specifies the attributes for a new or updated ruleset bypass actor. Only one of +`actor_id`, `repository_role_database_id`, or `organization_admin` should be specified. +""" +input RepositoryRulesetBypassActorInput { + """ + For Team and Integration bypasses, the Team or Integration ID + """ + actorId: ID + + """ + The bypass mode for this actor. + """ + bypassMode: RepositoryRulesetBypassActorBypassMode! + + """ + For org admin bupasses, true + """ + organizationAdmin: Boolean + + """ + For role bypasses, the role database ID + """ + repositoryRoleDatabaseId: Int +} + +""" +The connection type for RepositoryRuleset. +""" +type RepositoryRulesetConnection { + """ + A list of edges. + """ + edges: [RepositoryRulesetEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryRuleset] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryRulesetEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryRuleset +} + +""" +The targets supported for rulesets +""" +enum RepositoryRulesetTarget { + """ + Branch + """ + BRANCH + + """ + Tag + """ + TAG +} + +""" +A repository-topic connects a repository to a topic. +""" +type RepositoryTopic implements Node & UniformResourceLocatable { + id: ID! + + """ + The HTTP path for this repository-topic. + """ + resourcePath: URI! + + """ + The topic. + """ + topic: Topic! + + """ + The HTTP URL for this repository-topic. + """ + url: URI! +} + +""" +The connection type for RepositoryTopic. +""" +type RepositoryTopicConnection { + """ + A list of edges. + """ + edges: [RepositoryTopicEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryTopic] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryTopicEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryTopic +} + +""" +The repository's visibility level. +""" +enum RepositoryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repository_visibility_change.disable event. +""" +type RepositoryVisibilityChangeDisableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repository_visibility_change.enable event. +""" +type RepositoryVisibilityChangeEnableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +A Dependabot alert for a repository with a dependency affected by a security vulnerability. +""" +type RepositoryVulnerabilityAlert implements Node & RepositoryNode { + """ + When was the alert auto-dismissed? + """ + autoDismissedAt: DateTime + + """ + When was the alert created? + """ + createdAt: DateTime! + + """ + The associated Dependabot update + """ + dependabotUpdate: DependabotUpdate + + """ + The scope of an alert's dependency + """ + dependencyScope: RepositoryVulnerabilityAlertDependencyScope + + """ + Comment explaining the reason the alert was dismissed + """ + dismissComment: String + + """ + The reason the alert was dismissed + """ + dismissReason: String + + """ + When was the alert dismissed? + """ + dismissedAt: DateTime + + """ + The user who dismissed the alert + """ + dismisser: User + + """ + When was the alert fixed? + """ + fixedAt: DateTime + id: ID! + + """ + Identifies the alert number. + """ + number: Int! + + """ + The associated repository + """ + repository: Repository! + + """ + The associated security advisory + """ + securityAdvisory: SecurityAdvisory + + """ + The associated security vulnerability + """ + securityVulnerability: SecurityVulnerability + + """ + Identifies the state of the alert. + """ + state: RepositoryVulnerabilityAlertState! + + """ + The vulnerable manifest filename + """ + vulnerableManifestFilename: String! + + """ + The vulnerable manifest path + """ + vulnerableManifestPath: String! + + """ + The vulnerable requirements + """ + vulnerableRequirements: String +} + +""" +The connection type for RepositoryVulnerabilityAlert. +""" +type RepositoryVulnerabilityAlertConnection { + """ + A list of edges. + """ + edges: [RepositoryVulnerabilityAlertEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryVulnerabilityAlert] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The possible scopes of an alert's dependency. +""" +enum RepositoryVulnerabilityAlertDependencyScope { + """ + A dependency that is only used in development + """ + DEVELOPMENT + + """ + A dependency that is leveraged during application runtime + """ + RUNTIME +} + +""" +An edge in a connection. +""" +type RepositoryVulnerabilityAlertEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryVulnerabilityAlert +} + +""" +The possible states of an alert +""" +enum RepositoryVulnerabilityAlertState { + """ + An alert that has been automatically closed by Dependabot. + """ + AUTO_DISMISSED + + """ + An alert that has been manually closed by a user. + """ + DISMISSED + + """ + An alert that has been resolved by a code change. + """ + FIXED + + """ + An alert that is still open. + """ + OPEN +} + +""" +Autogenerated input type of RequestReviews +""" +input RequestReviewsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pull request to modify. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The Node IDs of the team to request. + """ + teamIds: [ID!] @possibleTypes(concreteTypes: ["Team"]) + + """ + Add users to the set rather than replace. + """ + union: Boolean = false + + """ + The Node IDs of the user to request. + """ + userIds: [ID!] @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of RequestReviews +""" +type RequestReviewsPayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that is getting requests. + """ + pullRequest: PullRequest + + """ + The edge from the pull request to the requested reviewers. + """ + requestedReviewersEdge: UserEdge +} + +""" +The possible states that can be requested when creating a check run. +""" +enum RequestableCheckStatusState { + """ + The check suite or run has been completed. + """ + COMPLETED + + """ + The check suite or run is in progress. + """ + IN_PROGRESS + + """ + The check suite or run is in pending state. + """ + PENDING + + """ + The check suite or run has been queued. + """ + QUEUED + + """ + The check suite or run is in waiting state. + """ + WAITING +} + +""" +Types that can be requested reviewers. +""" +union RequestedReviewer = Bot | Mannequin | Team | User + +""" +The connection type for RequestedReviewer. +""" +type RequestedReviewerConnection { + """ + A list of edges. + """ + edges: [RequestedReviewerEdge] + + """ + A list of nodes. + """ + nodes: [RequestedReviewer] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RequestedReviewerEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RequestedReviewer +} + +""" +Represents a type that can be required by a pull request for merging. +""" +interface RequirableByPullRequest { + """ + Whether this is required to pass before merging for a specific pull request. + """ + isRequired( + """ + The id of the pull request this is required for + """ + pullRequestId: ID + + """ + The number of the pull request this is required for + """ + pullRequestNumber: Int + ): Boolean! +} + +""" +Choose which environments must be successfully deployed to before branches can +be merged into a branch that matches this rule. +""" +type RequiredDeploymentsParameters { + """ + The environments that must be successfully deployed to before branches can be merged. + """ + requiredDeploymentEnvironments: [String!]! +} + +""" +Choose which environments must be successfully deployed to before branches can +be merged into a branch that matches this rule. +""" +input RequiredDeploymentsParametersInput { + """ + The environments that must be successfully deployed to before branches can be merged. + """ + requiredDeploymentEnvironments: [String!]! +} + +""" +Represents a required status check for a protected branch, but not any specific run of that check. +""" +type RequiredStatusCheckDescription { + """ + The App that must provide this status in order for it to be accepted. + """ + app: App + + """ + The name of this status. + """ + context: String! +} + +""" +Specifies the attributes for a new or updated required status check. +""" +input RequiredStatusCheckInput { + """ + The ID of the App that must set the status in order for it to be accepted. + Omit this value to use whichever app has recently been setting this status, or + use "any" to allow any app to set the status. + """ + appId: ID + + """ + Status check context that must pass for commits to be accepted to the matching branch. + """ + context: String! +} + +""" +Choose which status checks must pass before branches can be merged into a branch +that matches this rule. When enabled, commits must first be pushed to another +branch, then merged or pushed directly to a branch that matches this rule after +status checks have passed. +""" +type RequiredStatusChecksParameters { + """ + Status checks that are required. + """ + requiredStatusChecks: [StatusCheckConfiguration!]! + + """ + Whether pull requests targeting a matching branch must be tested with the + latest code. This setting will not take effect unless at least one status + check is enabled. + """ + strictRequiredStatusChecksPolicy: Boolean! +} + +""" +Choose which status checks must pass before branches can be merged into a branch +that matches this rule. When enabled, commits must first be pushed to another +branch, then merged or pushed directly to a branch that matches this rule after +status checks have passed. +""" +input RequiredStatusChecksParametersInput { + """ + Status checks that are required. + """ + requiredStatusChecks: [StatusCheckConfigurationInput!]! + + """ + Whether pull requests targeting a matching branch must be tested with the + latest code. This setting will not take effect unless at least one status + check is enabled. + """ + strictRequiredStatusChecksPolicy: Boolean! +} + +""" +Autogenerated input type of RerequestCheckSuite +""" +input RerequestCheckSuiteInput { + """ + The Node ID of the check suite. + """ + checkSuiteId: ID! @possibleTypes(concreteTypes: ["CheckSuite"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of RerequestCheckSuite +""" +type RerequestCheckSuitePayload { + """ + The requested check suite. + """ + checkSuite: CheckSuite + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of ResolveReviewThread +""" +input ResolveReviewThreadInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the thread to resolve + """ + threadId: ID! @possibleTypes(concreteTypes: ["PullRequestReviewThread"]) +} + +""" +Autogenerated return type of ResolveReviewThread +""" +type ResolveReviewThreadPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The thread to resolve. + """ + thread: PullRequestReviewThread +} + +""" +Represents a private contribution a user made on GitHub. +""" +type RestrictedContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +Autogenerated input type of RetireSponsorsTier +""" +input RetireSponsorsTierInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the published tier to retire. + """ + tierId: ID! @possibleTypes(concreteTypes: ["SponsorsTier"]) +} + +""" +Autogenerated return type of RetireSponsorsTier +""" +type RetireSponsorsTierPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The tier that was retired. + """ + sponsorsTier: SponsorsTier +} + +""" +Autogenerated input type of RevertPullRequest +""" +input RevertPullRequestInput { + """ + The description of the revert pull request. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Indicates whether the revert pull request should be a draft. + """ + draft: Boolean = false + + """ + The ID of the pull request to revert. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The title of the revert pull request. + """ + title: String +} + +""" +Autogenerated return type of RevertPullRequest +""" +type RevertPullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that was reverted. + """ + pullRequest: PullRequest + + """ + The new pull request that reverts the input pull request. + """ + revertPullRequest: PullRequest +} + +""" +A user, team, or app who has the ability to dismiss a review on a protected branch. +""" +type ReviewDismissalAllowance implements Node { + """ + The actor that can dismiss. + """ + actor: ReviewDismissalAllowanceActor + + """ + Identifies the branch protection rule associated with the allowed user, team, or app. + """ + branchProtectionRule: BranchProtectionRule + id: ID! +} + +""" +Types that can be an actor. +""" +union ReviewDismissalAllowanceActor = App | Team | User + +""" +The connection type for ReviewDismissalAllowance. +""" +type ReviewDismissalAllowanceConnection { + """ + A list of edges. + """ + edges: [ReviewDismissalAllowanceEdge] + + """ + A list of nodes. + """ + nodes: [ReviewDismissalAllowance] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReviewDismissalAllowanceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ReviewDismissalAllowance +} + +""" +Represents a 'review_dismissed' event on a given issue or pull request. +""" +type ReviewDismissedEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + Identifies the optional message associated with the 'review_dismissed' event. + """ + dismissalMessage: String + + """ + Identifies the optional message associated with the event, rendered to HTML. + """ + dismissalMessageHTML: String + id: ID! + + """ + Identifies the previous state of the review with the 'review_dismissed' event. + """ + previousReviewState: PullRequestReviewState! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the commit which caused the review to become stale. + """ + pullRequestCommit: PullRequestCommit + + """ + The HTTP path for this review dismissed event. + """ + resourcePath: URI! + + """ + Identifies the review associated with the 'review_dismissed' event. + """ + review: PullRequestReview + + """ + The HTTP URL for this review dismissed event. + """ + url: URI! +} + +""" +A request for a user to review a pull request. +""" +type ReviewRequest implements Node { + """ + Whether this request was created for a code owner + """ + asCodeOwner: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Identifies the pull request associated with this review request. + """ + pullRequest: PullRequest! + + """ + The reviewer that is requested. + """ + requestedReviewer: RequestedReviewer +} + +""" +The connection type for ReviewRequest. +""" +type ReviewRequestConnection { + """ + A list of edges. + """ + edges: [ReviewRequestEdge] + + """ + A list of nodes. + """ + nodes: [ReviewRequest] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReviewRequestEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ReviewRequest +} + +""" +Represents an 'review_request_removed' event on a given pull request. +""" +type ReviewRequestRemovedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the reviewer whose review request was removed. + """ + requestedReviewer: RequestedReviewer +} + +""" +Represents an 'review_requested' event on a given pull request. +""" +type ReviewRequestedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the reviewer whose review was requested. + """ + requestedReviewer: RequestedReviewer +} + +""" +A hovercard context with a message describing the current code review state of the pull +request. +""" +type ReviewStatusHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! + + """ + The current status of the pull request with respect to code review. + """ + reviewDecision: PullRequestReviewDecision +} + +""" +Autogenerated input type of RevokeEnterpriseOrganizationsMigratorRole +""" +input RevokeEnterpriseOrganizationsMigratorRoleInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise to which all organizations managed by it will be granted the migrator role. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of the user to revoke the migrator role + """ + login: String! +} + +""" +Autogenerated return type of RevokeEnterpriseOrganizationsMigratorRole +""" +type RevokeEnterpriseOrganizationsMigratorRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organizations that had the migrator role revoked for the given user. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationConnection +} + +""" +Autogenerated input type of RevokeMigratorRole +""" +input RevokeMigratorRoleInput { + """ + The user login or Team slug to revoke the migrator role from. + """ + actor: String! + + """ + Specifies the type of the actor, can be either USER or TEAM. + """ + actorType: ActorType! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the organization that the user/team belongs to. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of RevokeMigratorRole +""" +type RevokeMigratorRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Did the operation succeed? + """ + success: Boolean +} + +""" +Possible roles a user may have in relation to an organization. +""" +enum RoleInOrganization { + """ + A user who is a direct member of the organization. + """ + DIRECT_MEMBER + + """ + A user with full administrative access to the organization. + """ + OWNER + + """ + A user who is unaffiliated with the organization. + """ + UNAFFILIATED +} + +""" +The level of enforcement for a rule or ruleset. +""" +enum RuleEnforcement { + """ + Rules will be enforced + """ + ACTIVE + + """ + Do not evaluate or enforce rules + """ + DISABLED + + """ + Allow admins to test rules before enforcing them. Admins can view insights on + the Rule Insights page (`evaluate` is only available with GitHub Enterprise). + """ + EVALUATE +} + +""" +Types which can be parameters for `RepositoryRule` objects. +""" +union RuleParameters = + BranchNamePatternParameters + | CommitAuthorEmailPatternParameters + | CommitMessagePatternParameters + | CommitterEmailPatternParameters + | PullRequestParameters + | RequiredDeploymentsParameters + | RequiredStatusChecksParameters + | TagNamePatternParameters + | UpdateParameters + +""" +Specifies the parameters for a `RepositoryRule` object. Only one of the fields should be specified. +""" +input RuleParametersInput { + """ + Parameters used for the `branch_name_pattern` rule type + """ + branchNamePattern: BranchNamePatternParametersInput + + """ + Parameters used for the `commit_author_email_pattern` rule type + """ + commitAuthorEmailPattern: CommitAuthorEmailPatternParametersInput + + """ + Parameters used for the `commit_message_pattern` rule type + """ + commitMessagePattern: CommitMessagePatternParametersInput + + """ + Parameters used for the `committer_email_pattern` rule type + """ + committerEmailPattern: CommitterEmailPatternParametersInput + + """ + Parameters used for the `pull_request` rule type + """ + pullRequest: PullRequestParametersInput + + """ + Parameters used for the `required_deployments` rule type + """ + requiredDeployments: RequiredDeploymentsParametersInput + + """ + Parameters used for the `required_status_checks` rule type + """ + requiredStatusChecks: RequiredStatusChecksParametersInput + + """ + Parameters used for the `tag_name_pattern` rule type + """ + tagNamePattern: TagNamePatternParametersInput + + """ + Parameters used for the `update` rule type + """ + update: UpdateParametersInput +} + +""" +Types which can have `RepositoryRule` objects. +""" +union RuleSource = Organization | Repository + +""" +The possible digest algorithms used to sign SAML requests for an identity provider. +""" +enum SamlDigestAlgorithm { + """ + SHA1 + """ + SHA1 + + """ + SHA256 + """ + SHA256 + + """ + SHA384 + """ + SHA384 + + """ + SHA512 + """ + SHA512 +} + +""" +The possible signature algorithms used to sign SAML requests for a Identity Provider. +""" +enum SamlSignatureAlgorithm { + """ + RSA-SHA1 + """ + RSA_SHA1 + + """ + RSA-SHA256 + """ + RSA_SHA256 + + """ + RSA-SHA384 + """ + RSA_SHA384 + + """ + RSA-SHA512 + """ + RSA_SHA512 +} + +""" +A Saved Reply is text a user can use to reply quickly. +""" +type SavedReply implements Node { + """ + The body of the saved reply. + """ + body: String! + + """ + The saved reply body rendered to HTML. + """ + bodyHTML: HTML! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The title of the saved reply. + """ + title: String! + + """ + The user that saved this reply. + """ + user: Actor +} + +""" +The connection type for SavedReply. +""" +type SavedReplyConnection { + """ + A list of edges. + """ + edges: [SavedReplyEdge] + + """ + A list of nodes. + """ + nodes: [SavedReply] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SavedReplyEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SavedReply +} + +""" +Ordering options for saved reply connections. +""" +input SavedReplyOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order saved replies by. + """ + field: SavedReplyOrderField! +} + +""" +Properties by which saved reply connections can be ordered. +""" +enum SavedReplyOrderField { + """ + Order saved reply by when they were updated. + """ + UPDATED_AT +} + +""" +The results of a search. +""" +union SearchResultItem = App | Discussion | Issue | MarketplaceListing | Organization | PullRequest | Repository | User + +""" +A list of results that matched against a search query. Regardless of the number +of matches, a maximum of 1,000 results will be available across all types, +potentially split across many pages. +""" +type SearchResultItemConnection { + """ + The total number of pieces of code that matched the search query. Regardless + of the total number of matches, a maximum of 1,000 results will be available + across all types. + """ + codeCount: Int! + + """ + The total number of discussions that matched the search query. Regardless of + the total number of matches, a maximum of 1,000 results will be available + across all types. + """ + discussionCount: Int! + + """ + A list of edges. + """ + edges: [SearchResultItemEdge] + + """ + The total number of issues that matched the search query. Regardless of the + total number of matches, a maximum of 1,000 results will be available across all types. + """ + issueCount: Int! + + """ + A list of nodes. + """ + nodes: [SearchResultItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total number of repositories that matched the search query. Regardless of + the total number of matches, a maximum of 1,000 results will be available + across all types. + """ + repositoryCount: Int! + + """ + The total number of users that matched the search query. Regardless of the + total number of matches, a maximum of 1,000 results will be available across all types. + """ + userCount: Int! + + """ + The total number of wiki pages that matched the search query. Regardless of + the total number of matches, a maximum of 1,000 results will be available + across all types. + """ + wikiCount: Int! +} + +""" +An edge in a connection. +""" +type SearchResultItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SearchResultItem + + """ + Text matches on the result found. + """ + textMatches: [TextMatch] +} + +""" +Represents the individual results of a search. +""" +enum SearchType { + """ + Returns matching discussions in repositories. + """ + DISCUSSION + + """ + Returns results matching issues in repositories. + """ + ISSUE + + """ + Returns results matching repositories. + """ + REPOSITORY + + """ + Returns results matching users and organizations on GitHub. + """ + USER +} + +""" +A GitHub Security Advisory +""" +type SecurityAdvisory implements Node { + """ + The classification of the advisory + """ + classification: SecurityAdvisoryClassification! + + """ + The CVSS associated with this advisory + """ + cvss: CVSS! + + """ + CWEs associated with this Advisory + """ + cwes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CWEConnection! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + This is a long plaintext description of the advisory + """ + description: String! + + """ + The GitHub Security Advisory ID + """ + ghsaId: String! + id: ID! + + """ + A list of identifiers for this advisory + """ + identifiers: [SecurityAdvisoryIdentifier!]! + + """ + The permalink for the advisory's dependabot alerts page + """ + notificationsPermalink: URI + + """ + The organization that originated the advisory + """ + origin: String! + + """ + The permalink for the advisory + """ + permalink: URI + + """ + When the advisory was published + """ + publishedAt: DateTime! + + """ + A list of references for this advisory + """ + references: [SecurityAdvisoryReference!]! + + """ + The severity of the advisory + """ + severity: SecurityAdvisorySeverity! + + """ + A short plaintext summary of the advisory + """ + summary: String! + + """ + When the advisory was last updated + """ + updatedAt: DateTime! + + """ + Vulnerabilities associated with this Advisory + """ + vulnerabilities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + A list of advisory classifications to filter vulnerabilities by. + """ + classifications: [SecurityAdvisoryClassification!] + + """ + An ecosystem to filter vulnerabilities by. + """ + ecosystem: SecurityAdvisoryEcosystem + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the returned topics. + """ + orderBy: SecurityVulnerabilityOrder = {field: UPDATED_AT, direction: DESC} + + """ + A package name to filter vulnerabilities by. + """ + package: String + + """ + A list of severities to filter vulnerabilities by. + """ + severities: [SecurityAdvisorySeverity!] + ): SecurityVulnerabilityConnection! + + """ + When the advisory was withdrawn, if it has been withdrawn + """ + withdrawnAt: DateTime +} + +""" +Classification of the advisory. +""" +enum SecurityAdvisoryClassification { + """ + Classification of general advisories. + """ + GENERAL + + """ + Classification of malware advisories. + """ + MALWARE +} + +""" +The connection type for SecurityAdvisory. +""" +type SecurityAdvisoryConnection { + """ + A list of edges. + """ + edges: [SecurityAdvisoryEdge] + + """ + A list of nodes. + """ + nodes: [SecurityAdvisory] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The possible ecosystems of a security vulnerability's package. +""" +enum SecurityAdvisoryEcosystem { + """ + GitHub Actions + """ + ACTIONS + + """ + PHP packages hosted at packagist.org + """ + COMPOSER + + """ + Erlang/Elixir packages hosted at hex.pm + """ + ERLANG + + """ + Go modules + """ + GO + + """ + Java artifacts hosted at the Maven central repository + """ + MAVEN + + """ + JavaScript packages hosted at npmjs.com + """ + NPM + + """ + .NET packages hosted at the NuGet Gallery + """ + NUGET + + """ + Python packages hosted at PyPI.org + """ + PIP + + """ + Dart packages hosted at pub.dev + """ + PUB + + """ + Ruby gems hosted at RubyGems.org + """ + RUBYGEMS + + """ + Rust crates + """ + RUST + + """ + Swift packages + """ + SWIFT +} + +""" +An edge in a connection. +""" +type SecurityAdvisoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SecurityAdvisory +} + +""" +A GitHub Security Advisory Identifier +""" +type SecurityAdvisoryIdentifier { + """ + The identifier type, e.g. GHSA, CVE + """ + type: String! + + """ + The identifier + """ + value: String! +} + +""" +An advisory identifier to filter results on. +""" +input SecurityAdvisoryIdentifierFilter { + """ + The identifier type. + """ + type: SecurityAdvisoryIdentifierType! + + """ + The identifier string. Supports exact or partial matching. + """ + value: String! +} + +""" +Identifier formats available for advisories. +""" +enum SecurityAdvisoryIdentifierType { + """ + Common Vulnerabilities and Exposures Identifier. + """ + CVE + + """ + GitHub Security Advisory ID. + """ + GHSA +} + +""" +Ordering options for security advisory connections +""" +input SecurityAdvisoryOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order security advisories by. + """ + field: SecurityAdvisoryOrderField! +} + +""" +Properties by which security advisory connections can be ordered. +""" +enum SecurityAdvisoryOrderField { + """ + Order advisories by publication time + """ + PUBLISHED_AT + + """ + Order advisories by update time + """ + UPDATED_AT +} + +""" +An individual package +""" +type SecurityAdvisoryPackage { + """ + The ecosystem the package belongs to, e.g. RUBYGEMS, NPM + """ + ecosystem: SecurityAdvisoryEcosystem! + + """ + The package name + """ + name: String! +} + +""" +An individual package version +""" +type SecurityAdvisoryPackageVersion { + """ + The package name or version + """ + identifier: String! +} + +""" +A GitHub Security Advisory Reference +""" +type SecurityAdvisoryReference { + """ + A publicly accessible reference + """ + url: URI! +} + +""" +Severity of the vulnerability. +""" +enum SecurityAdvisorySeverity { + """ + Critical. + """ + CRITICAL + + """ + High. + """ + HIGH + + """ + Low. + """ + LOW + + """ + Moderate. + """ + MODERATE +} + +""" +An individual vulnerability within an Advisory +""" +type SecurityVulnerability { + """ + The Advisory associated with this Vulnerability + """ + advisory: SecurityAdvisory! + + """ + The first version containing a fix for the vulnerability + """ + firstPatchedVersion: SecurityAdvisoryPackageVersion + + """ + A description of the vulnerable package + """ + package: SecurityAdvisoryPackage! + + """ + The severity of the vulnerability within this package + """ + severity: SecurityAdvisorySeverity! + + """ + When the vulnerability was last updated + """ + updatedAt: DateTime! + + """ + A string that describes the vulnerable package versions. + This string follows a basic syntax with a few forms. + + `= 0.2.0` denotes a single vulnerable version. + + `<= 1.0.8` denotes a version range up to and including the specified version + + `< 0.1.11` denotes a version range up to, but excluding, the specified version + + `>= 4.3.0, < 4.3.5` denotes a version range with a known minimum and maximum version. + + `>= 0.0.1` denotes a version range with a known minimum, but no known maximum + """ + vulnerableVersionRange: String! +} + +""" +The connection type for SecurityVulnerability. +""" +type SecurityVulnerabilityConnection { + """ + A list of edges. + """ + edges: [SecurityVulnerabilityEdge] + + """ + A list of nodes. + """ + nodes: [SecurityVulnerability] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SecurityVulnerabilityEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SecurityVulnerability +} + +""" +Ordering options for security vulnerability connections +""" +input SecurityVulnerabilityOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order security vulnerabilities by. + """ + field: SecurityVulnerabilityOrderField! +} + +""" +Properties by which security vulnerability connections can be ordered. +""" +enum SecurityVulnerabilityOrderField { + """ + Order vulnerability by update time + """ + UPDATED_AT +} + +""" +Autogenerated input type of SetEnterpriseIdentityProvider +""" +input SetEnterpriseIdentityProviderInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The digest algorithm used to sign SAML requests for the identity provider. + """ + digestMethod: SamlDigestAlgorithm! + + """ + The ID of the enterprise on which to set an identity provider. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The x509 certificate used by the identity provider to sign assertions and responses. + """ + idpCertificate: String! + + """ + The Issuer Entity ID for the SAML identity provider + """ + issuer: String + + """ + The signature algorithm used to sign SAML requests for the identity provider. + """ + signatureMethod: SamlSignatureAlgorithm! + + """ + The URL endpoint for the identity provider's SAML SSO. + """ + ssoUrl: URI! +} + +""" +Autogenerated return type of SetEnterpriseIdentityProvider +""" +type SetEnterpriseIdentityProviderPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The identity provider for the enterprise. + """ + identityProvider: EnterpriseIdentityProvider +} + +""" +Autogenerated input type of SetOrganizationInteractionLimit +""" +input SetOrganizationInteractionLimitInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + When this limit should expire. + """ + expiry: RepositoryInteractionLimitExpiry + + """ + The limit to set. + """ + limit: RepositoryInteractionLimit! + + """ + The ID of the organization to set a limit for. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of SetOrganizationInteractionLimit +""" +type SetOrganizationInteractionLimitPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organization that the interaction limit was set for. + """ + organization: Organization +} + +""" +Autogenerated input type of SetRepositoryInteractionLimit +""" +input SetRepositoryInteractionLimitInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + When this limit should expire. + """ + expiry: RepositoryInteractionLimitExpiry + + """ + The limit to set. + """ + limit: RepositoryInteractionLimit! + + """ + The ID of the repository to set a limit for. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of SetRepositoryInteractionLimit +""" +type SetRepositoryInteractionLimitPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository that the interaction limit was set for. + """ + repository: Repository +} + +""" +Autogenerated input type of SetUserInteractionLimit +""" +input SetUserInteractionLimitInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + When this limit should expire. + """ + expiry: RepositoryInteractionLimitExpiry + + """ + The limit to set. + """ + limit: RepositoryInteractionLimit! + + """ + The ID of the user to set a limit for. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of SetUserInteractionLimit +""" +type SetUserInteractionLimitPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The user that the interaction limit was set for. + """ + user: User +} + +""" +Represents an S/MIME signature on a Commit or Tag. +""" +type SmimeSignature implements GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +Social media profile associated with a user. +""" +type SocialAccount { + """ + Name of the social media account as it appears on the profile. + """ + displayName: String! + + """ + Software or company that hosts the social media account. + """ + provider: SocialAccountProvider! + + """ + URL of the social media account. + """ + url: URI! +} + +""" +The connection type for SocialAccount. +""" +type SocialAccountConnection { + """ + A list of edges. + """ + edges: [SocialAccountEdge] + + """ + A list of nodes. + """ + nodes: [SocialAccount] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SocialAccountEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SocialAccount +} + +""" +Software or company that hosts social media accounts. +""" +enum SocialAccountProvider { + """ + Social media and networking website. + """ + FACEBOOK + + """ + Catch-all for social media providers that do not yet have specific handling. + """ + GENERIC + + """ + Fork of Mastodon with a greater focus on local posting. + """ + HOMETOWN + + """ + Social media website with a focus on photo and video sharing. + """ + INSTAGRAM + + """ + Professional networking website. + """ + LINKEDIN + + """ + Open-source federated microblogging service. + """ + MASTODON + + """ + Social news aggregation and discussion website. + """ + REDDIT + + """ + Live-streaming service. + """ + TWITCH + + """ + Microblogging website. + """ + TWITTER + + """ + Online video platform. + """ + YOUTUBE +} + +""" +Entities that can sponsor others via GitHub Sponsors +""" +union Sponsor = Organization | User + +""" +The connection type for Sponsor. +""" +type SponsorConnection { + """ + A list of edges. + """ + edges: [SponsorEdge] + + """ + A list of nodes. + """ + nodes: [Sponsor] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user or organization who is sponsoring someone in GitHub Sponsors. +""" +type SponsorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Sponsor +} + +""" +Ordering options for connections to get sponsor entities for GitHub Sponsors. +""" +input SponsorOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order sponsor entities by. + """ + field: SponsorOrderField! +} + +""" +Properties by which sponsor connections can be ordered. +""" +enum SponsorOrderField { + """ + Order sponsorable entities by login (username). + """ + LOGIN + + """ + Order sponsors by their relevance to the viewer. + """ + RELEVANCE +} + +""" +Entities that can sponsor or be sponsored through GitHub Sponsors. +""" +interface Sponsorable { + """ + The estimated next GitHub Sponsors payout for this user/organization in cents (USD). + """ + estimatedNextSponsorsPayoutInCents: Int! + + """ + True if this user/organization has a GitHub Sponsors listing. + """ + hasSponsorsListing: Boolean! + + """ + Whether the given account is sponsoring this user/organization. + """ + isSponsoredBy( + """ + The target account's login. + """ + accountLogin: String! + ): Boolean! + + """ + True if the viewer is sponsored by this user/organization. + """ + isSponsoringViewer: Boolean! + + """ + The estimated monthly GitHub Sponsors income for this user/organization in cents (USD). + """ + monthlyEstimatedSponsorsIncomeInCents: Int! + + """ + List of users and organizations this entity is sponsoring. + """ + sponsoring( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the users and organizations returned from the connection. + """ + orderBy: SponsorOrder = {field: RELEVANCE, direction: DESC} + ): SponsorConnection! + + """ + List of sponsors for this user or organization. + """ + sponsors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsors returned from the connection. + """ + orderBy: SponsorOrder = {field: RELEVANCE, direction: DESC} + + """ + If given, will filter for sponsors at the given tier. Will only return + sponsors whose tier the viewer is permitted to see. + """ + tierId: ID + ): SponsorConnection! + + """ + Events involving this sponsorable, such as new sponsorships. + """ + sponsorsActivities( + """ + Filter activities to only the specified actions. + """ + actions: [SponsorsActivityAction!] = [] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether to include those events where this sponsorable acted as the sponsor. + Defaults to only including events where this sponsorable was the recipient + of a sponsorship. + """ + includeAsSponsor: Boolean = false + + """ + Whether or not to include private activities in the result set. Defaults to including public and private activities. + """ + includePrivate: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for activity returned from the connection. + """ + orderBy: SponsorsActivityOrder = {field: TIMESTAMP, direction: DESC} + + """ + Filter activities returned to only those that occurred in the most recent + specified time period. Set to ALL to avoid filtering by when the activity + occurred. Will be ignored if `since` or `until` is given. + """ + period: SponsorsActivityPeriod = MONTH + + """ + Filter activities to those that occurred on or after this time. + """ + since: DateTime + + """ + Filter activities to those that occurred before this time. + """ + until: DateTime + ): SponsorsActivityConnection! + + """ + The GitHub Sponsors listing for this user or organization. + """ + sponsorsListing: SponsorsListing + + """ + The sponsorship from the viewer to this user/organization; that is, the sponsorship where you're the sponsor. + """ + sponsorshipForViewerAsSponsor( + """ + Whether to return the sponsorship only if it's still active. Pass false to + get the viewer's sponsorship back even if it has been cancelled. + """ + activeOnly: Boolean = true + ): Sponsorship + + """ + The sponsorship from this user/organization to the viewer; that is, the sponsorship you're receiving. + """ + sponsorshipForViewerAsSponsorable( + """ + Whether to return the sponsorship only if it's still active. Pass false to + get the sponsorship back even if it has been cancelled. + """ + activeOnly: Boolean = true + ): Sponsorship + + """ + List of sponsorship updates sent from this sponsorable to sponsors. + """ + sponsorshipNewsletters( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorship updates returned from the connection. + """ + orderBy: SponsorshipNewsletterOrder = {field: CREATED_AT, direction: DESC} + ): SponsorshipNewsletterConnection! + + """ + The sponsorships where this user or organization is the maintainer receiving the funds. + """ + sponsorshipsAsMaintainer( + """ + Whether to include only sponsorships that are active right now, versus all + sponsorships this maintainer has ever received. + """ + activeOnly: Boolean = true + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether or not to include private sponsorships in the result set + """ + includePrivate: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + The sponsorships where this user or organization is the funder. + """ + sponsorshipsAsSponsor( + """ + Whether to include only sponsorships that are active right now, versus all sponsorships this sponsor has ever made. + """ + activeOnly: Boolean = true + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter sponsorships returned to those for the specified maintainers. That + is, the recipient of the sponsorship is a user or organization with one of + the given logins. + """ + maintainerLogins: [String!] + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + The amount in United States cents (e.g., 500 = $5.00 USD) that this entity has + spent on GitHub to fund sponsorships. Only returns a value when viewed by the + user themselves or by a user who can manage sponsorships for the requested organization. + """ + totalSponsorshipAmountAsSponsorInCents( + """ + Filter payments to those that occurred on or after this time. + """ + since: DateTime + + """ + Filter payments to those made to the users or organizations with the specified usernames. + """ + sponsorableLogins: [String!] = [] + + """ + Filter payments to those that occurred before this time. + """ + until: DateTime + ): Int + + """ + Whether or not the viewer is able to sponsor this user/organization. + """ + viewerCanSponsor: Boolean! + + """ + True if the viewer is sponsoring this user/organization. + """ + viewerIsSponsoring: Boolean! +} + +""" +Entities that can be sponsored via GitHub Sponsors +""" +union SponsorableItem = Organization | User + +""" +The connection type for SponsorableItem. +""" +type SponsorableItemConnection { + """ + A list of edges. + """ + edges: [SponsorableItemEdge] + + """ + A list of nodes. + """ + nodes: [SponsorableItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SponsorableItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SponsorableItem +} + +""" +Ordering options for connections to get sponsorable entities for GitHub Sponsors. +""" +input SponsorableOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order sponsorable entities by. + """ + field: SponsorableOrderField! +} + +""" +Properties by which sponsorable connections can be ordered. +""" +enum SponsorableOrderField { + """ + Order sponsorable entities by login (username). + """ + LOGIN +} + +""" +An event related to sponsorship activity. +""" +type SponsorsActivity implements Node { + """ + What action this activity indicates took place. + """ + action: SponsorsActivityAction! + + """ + The sponsor's current privacy level. + """ + currentPrivacyLevel: SponsorshipPrivacy + id: ID! + + """ + The tier that the sponsorship used to use, for tier change events. + """ + previousSponsorsTier: SponsorsTier + + """ + The user or organization who triggered this activity and was/is sponsoring the sponsorable. + """ + sponsor: Sponsor + + """ + The user or organization that is being sponsored, the maintainer. + """ + sponsorable: Sponsorable! + + """ + The associated sponsorship tier. + """ + sponsorsTier: SponsorsTier + + """ + The timestamp of this event. + """ + timestamp: DateTime + + """ + Was this sponsorship made alongside other sponsorships at the same time from the same sponsor? + """ + viaBulkSponsorship: Boolean! +} + +""" +The possible actions that GitHub Sponsors activities can represent. +""" +enum SponsorsActivityAction { + """ + The activity was cancelling a sponsorship. + """ + CANCELLED_SPONSORSHIP + + """ + The activity was starting a sponsorship. + """ + NEW_SPONSORSHIP + + """ + The activity was scheduling a downgrade or cancellation. + """ + PENDING_CHANGE + + """ + The activity was funds being refunded to the sponsor or GitHub. + """ + REFUND + + """ + The activity was disabling matching for a previously matched sponsorship. + """ + SPONSOR_MATCH_DISABLED + + """ + The activity was changing the sponsorship tier, either directly by the sponsor or by a scheduled/pending change. + """ + TIER_CHANGE +} + +""" +The connection type for SponsorsActivity. +""" +type SponsorsActivityConnection { + """ + A list of edges. + """ + edges: [SponsorsActivityEdge] + + """ + A list of nodes. + """ + nodes: [SponsorsActivity] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SponsorsActivityEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SponsorsActivity +} + +""" +Ordering options for GitHub Sponsors activity connections. +""" +input SponsorsActivityOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order activity by. + """ + field: SponsorsActivityOrderField! +} + +""" +Properties by which GitHub Sponsors activity connections can be ordered. +""" +enum SponsorsActivityOrderField { + """ + Order activities by when they happened. + """ + TIMESTAMP +} + +""" +The possible time periods for which Sponsors activities can be requested. +""" +enum SponsorsActivityPeriod { + """ + Don't restrict the activity to any date range, include all activity. + """ + ALL + + """ + The previous calendar day. + """ + DAY + + """ + The previous thirty days. + """ + MONTH + + """ + The previous seven days. + """ + WEEK +} + +""" +Represents countries or regions for billing and residence for a GitHub Sponsors profile. +""" +enum SponsorsCountryOrRegionCode { + """ + Andorra + """ + AD + + """ + United Arab Emirates + """ + AE + + """ + Afghanistan + """ + AF + + """ + Antigua and Barbuda + """ + AG + + """ + Anguilla + """ + AI + + """ + Albania + """ + AL + + """ + Armenia + """ + AM + + """ + Angola + """ + AO + + """ + Antarctica + """ + AQ + + """ + Argentina + """ + AR + + """ + American Samoa + """ + AS + + """ + Austria + """ + AT + + """ + Australia + """ + AU + + """ + Aruba + """ + AW + + """ + Åland + """ + AX + + """ + Azerbaijan + """ + AZ + + """ + Bosnia and Herzegovina + """ + BA + + """ + Barbados + """ + BB + + """ + Bangladesh + """ + BD + + """ + Belgium + """ + BE + + """ + Burkina Faso + """ + BF + + """ + Bulgaria + """ + BG + + """ + Bahrain + """ + BH + + """ + Burundi + """ + BI + + """ + Benin + """ + BJ + + """ + Saint Barthélemy + """ + BL + + """ + Bermuda + """ + BM + + """ + Brunei Darussalam + """ + BN + + """ + Bolivia + """ + BO + + """ + Bonaire, Sint Eustatius and Saba + """ + BQ + + """ + Brazil + """ + BR + + """ + Bahamas + """ + BS + + """ + Bhutan + """ + BT + + """ + Bouvet Island + """ + BV + + """ + Botswana + """ + BW + + """ + Belarus + """ + BY + + """ + Belize + """ + BZ + + """ + Canada + """ + CA + + """ + Cocos (Keeling) Islands + """ + CC + + """ + Congo (Kinshasa) + """ + CD + + """ + Central African Republic + """ + CF + + """ + Congo (Brazzaville) + """ + CG + + """ + Switzerland + """ + CH + + """ + Côte d'Ivoire + """ + CI + + """ + Cook Islands + """ + CK + + """ + Chile + """ + CL + + """ + Cameroon + """ + CM + + """ + China + """ + CN + + """ + Colombia + """ + CO + + """ + Costa Rica + """ + CR + + """ + Cape Verde + """ + CV + + """ + Curaçao + """ + CW + + """ + Christmas Island + """ + CX + + """ + Cyprus + """ + CY + + """ + Czech Republic + """ + CZ + + """ + Germany + """ + DE + + """ + Djibouti + """ + DJ + + """ + Denmark + """ + DK + + """ + Dominica + """ + DM + + """ + Dominican Republic + """ + DO + + """ + Algeria + """ + DZ + + """ + Ecuador + """ + EC + + """ + Estonia + """ + EE + + """ + Egypt + """ + EG + + """ + Western Sahara + """ + EH + + """ + Eritrea + """ + ER + + """ + Spain + """ + ES + + """ + Ethiopia + """ + ET + + """ + Finland + """ + FI + + """ + Fiji + """ + FJ + + """ + Falkland Islands + """ + FK + + """ + Micronesia + """ + FM + + """ + Faroe Islands + """ + FO + + """ + France + """ + FR + + """ + Gabon + """ + GA + + """ + United Kingdom + """ + GB + + """ + Grenada + """ + GD + + """ + Georgia + """ + GE + + """ + French Guiana + """ + GF + + """ + Guernsey + """ + GG + + """ + Ghana + """ + GH + + """ + Gibraltar + """ + GI + + """ + Greenland + """ + GL + + """ + Gambia + """ + GM + + """ + Guinea + """ + GN + + """ + Guadeloupe + """ + GP + + """ + Equatorial Guinea + """ + GQ + + """ + Greece + """ + GR + + """ + South Georgia and South Sandwich Islands + """ + GS + + """ + Guatemala + """ + GT + + """ + Guam + """ + GU + + """ + Guinea-Bissau + """ + GW + + """ + Guyana + """ + GY + + """ + Hong Kong + """ + HK + + """ + Heard and McDonald Islands + """ + HM + + """ + Honduras + """ + HN + + """ + Croatia + """ + HR + + """ + Haiti + """ + HT + + """ + Hungary + """ + HU + + """ + Indonesia + """ + ID + + """ + Ireland + """ + IE + + """ + Israel + """ + IL + + """ + Isle of Man + """ + IM + + """ + India + """ + IN + + """ + British Indian Ocean Territory + """ + IO + + """ + Iraq + """ + IQ + + """ + Iran + """ + IR + + """ + Iceland + """ + IS + + """ + Italy + """ + IT + + """ + Jersey + """ + JE + + """ + Jamaica + """ + JM + + """ + Jordan + """ + JO + + """ + Japan + """ + JP + + """ + Kenya + """ + KE + + """ + Kyrgyzstan + """ + KG + + """ + Cambodia + """ + KH + + """ + Kiribati + """ + KI + + """ + Comoros + """ + KM + + """ + Saint Kitts and Nevis + """ + KN + + """ + Korea, South + """ + KR + + """ + Kuwait + """ + KW + + """ + Cayman Islands + """ + KY + + """ + Kazakhstan + """ + KZ + + """ + Laos + """ + LA + + """ + Lebanon + """ + LB + + """ + Saint Lucia + """ + LC + + """ + Liechtenstein + """ + LI + + """ + Sri Lanka + """ + LK + + """ + Liberia + """ + LR + + """ + Lesotho + """ + LS + + """ + Lithuania + """ + LT + + """ + Luxembourg + """ + LU + + """ + Latvia + """ + LV + + """ + Libya + """ + LY + + """ + Morocco + """ + MA + + """ + Monaco + """ + MC + + """ + Moldova + """ + MD + + """ + Montenegro + """ + ME + + """ + Saint Martin (French part) + """ + MF + + """ + Madagascar + """ + MG + + """ + Marshall Islands + """ + MH + + """ + Macedonia + """ + MK + + """ + Mali + """ + ML + + """ + Myanmar + """ + MM + + """ + Mongolia + """ + MN + + """ + Macau + """ + MO + + """ + Northern Mariana Islands + """ + MP + + """ + Martinique + """ + MQ + + """ + Mauritania + """ + MR + + """ + Montserrat + """ + MS + + """ + Malta + """ + MT + + """ + Mauritius + """ + MU + + """ + Maldives + """ + MV + + """ + Malawi + """ + MW + + """ + Mexico + """ + MX + + """ + Malaysia + """ + MY + + """ + Mozambique + """ + MZ + + """ + Namibia + """ + NA + + """ + New Caledonia + """ + NC + + """ + Niger + """ + NE + + """ + Norfolk Island + """ + NF + + """ + Nigeria + """ + NG + + """ + Nicaragua + """ + NI + + """ + Netherlands + """ + NL + + """ + Norway + """ + NO + + """ + Nepal + """ + NP + + """ + Nauru + """ + NR + + """ + Niue + """ + NU + + """ + New Zealand + """ + NZ + + """ + Oman + """ + OM + + """ + Panama + """ + PA + + """ + Peru + """ + PE + + """ + French Polynesia + """ + PF + + """ + Papua New Guinea + """ + PG + + """ + Philippines + """ + PH + + """ + Pakistan + """ + PK + + """ + Poland + """ + PL + + """ + Saint Pierre and Miquelon + """ + PM + + """ + Pitcairn + """ + PN + + """ + Puerto Rico + """ + PR + + """ + Palestine + """ + PS + + """ + Portugal + """ + PT + + """ + Palau + """ + PW + + """ + Paraguay + """ + PY + + """ + Qatar + """ + QA + + """ + Reunion + """ + RE + + """ + Romania + """ + RO + + """ + Serbia + """ + RS + + """ + Russian Federation + """ + RU + + """ + Rwanda + """ + RW + + """ + Saudi Arabia + """ + SA + + """ + Solomon Islands + """ + SB + + """ + Seychelles + """ + SC + + """ + Sudan + """ + SD + + """ + Sweden + """ + SE + + """ + Singapore + """ + SG + + """ + Saint Helena + """ + SH + + """ + Slovenia + """ + SI + + """ + Svalbard and Jan Mayen Islands + """ + SJ + + """ + Slovakia + """ + SK + + """ + Sierra Leone + """ + SL + + """ + San Marino + """ + SM + + """ + Senegal + """ + SN + + """ + Somalia + """ + SO + + """ + Suriname + """ + SR + + """ + South Sudan + """ + SS + + """ + Sao Tome and Principe + """ + ST + + """ + El Salvador + """ + SV + + """ + Sint Maarten (Dutch part) + """ + SX + + """ + Swaziland + """ + SZ + + """ + Turks and Caicos Islands + """ + TC + + """ + Chad + """ + TD + + """ + French Southern Lands + """ + TF + + """ + Togo + """ + TG + + """ + Thailand + """ + TH + + """ + Tajikistan + """ + TJ + + """ + Tokelau + """ + TK + + """ + Timor-Leste + """ + TL + + """ + Turkmenistan + """ + TM + + """ + Tunisia + """ + TN + + """ + Tonga + """ + TO + + """ + Turkey + """ + TR + + """ + Trinidad and Tobago + """ + TT + + """ + Tuvalu + """ + TV + + """ + Taiwan + """ + TW + + """ + Tanzania + """ + TZ + + """ + Ukraine + """ + UA + + """ + Uganda + """ + UG + + """ + United States Minor Outlying Islands + """ + UM + + """ + United States of America + """ + US + + """ + Uruguay + """ + UY + + """ + Uzbekistan + """ + UZ + + """ + Vatican City + """ + VA + + """ + Saint Vincent and the Grenadines + """ + VC + + """ + Venezuela + """ + VE + + """ + Virgin Islands, British + """ + VG + + """ + Virgin Islands, U.S. + """ + VI + + """ + Vietnam + """ + VN + + """ + Vanuatu + """ + VU + + """ + Wallis and Futuna Islands + """ + WF + + """ + Samoa + """ + WS + + """ + Yemen + """ + YE + + """ + Mayotte + """ + YT + + """ + South Africa + """ + ZA + + """ + Zambia + """ + ZM + + """ + Zimbabwe + """ + ZW +} + +""" +A goal associated with a GitHub Sponsors listing, representing a target the sponsored maintainer would like to attain. +""" +type SponsorsGoal { + """ + A description of the goal from the maintainer. + """ + description: String + + """ + What the objective of this goal is. + """ + kind: SponsorsGoalKind! + + """ + The percentage representing how complete this goal is, between 0-100. + """ + percentComplete: Int! + + """ + What the goal amount is. Represents an amount in USD for monthly sponsorship + amount goals. Represents a count of unique sponsors for total sponsors count goals. + """ + targetValue: Int! + + """ + A brief summary of the kind and target value of this goal. + """ + title: String! +} + +""" +The different kinds of goals a GitHub Sponsors member can have. +""" +enum SponsorsGoalKind { + """ + The goal is about getting a certain amount in USD from sponsorships each month. + """ + MONTHLY_SPONSORSHIP_AMOUNT + + """ + The goal is about reaching a certain number of sponsors. + """ + TOTAL_SPONSORS_COUNT +} + +""" +A GitHub Sponsors listing. +""" +type SponsorsListing implements Node { + """ + The current goal the maintainer is trying to reach with GitHub Sponsors, if any. + """ + activeGoal: SponsorsGoal + + """ + The Stripe Connect account currently in use for payouts for this Sponsors + listing, if any. Will only return a value when queried by the maintainer + themselves, or by an admin of the sponsorable organization. + """ + activeStripeConnectAccount: StripeConnectAccount + + """ + The name of the country or region with the maintainer's bank account or fiscal + host. Will only return a value when queried by the maintainer themselves, or + by an admin of the sponsorable organization. + """ + billingCountryOrRegion: String + + """ + The email address used by GitHub to contact the sponsorable about their GitHub + Sponsors profile. Will only return a value when queried by the maintainer + themselves, or by an admin of the sponsorable organization. + """ + contactEmailAddress: String + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The HTTP path for the Sponsors dashboard for this Sponsors listing. + """ + dashboardResourcePath: URI! + + """ + The HTTP URL for the Sponsors dashboard for this Sponsors listing. + """ + dashboardUrl: URI! + + """ + The records featured on the GitHub Sponsors profile. + """ + featuredItems( + """ + The types of featured items to return. + """ + featureableTypes: [SponsorsListingFeaturedItemFeatureableType!] = [REPOSITORY, USER] + ): [SponsorsListingFeaturedItem!]! + + """ + The fiscal host used for payments, if any. Will only return a value when + queried by the maintainer themselves, or by an admin of the sponsorable organization. + """ + fiscalHost: Organization + + """ + The full description of the listing. + """ + fullDescription: String! + + """ + The full description of the listing rendered to HTML. + """ + fullDescriptionHTML: HTML! + id: ID! + + """ + Whether this listing is publicly visible. + """ + isPublic: Boolean! + + """ + The listing's full name. + """ + name: String! + + """ + A future date on which this listing is eligible to receive a payout. + """ + nextPayoutDate: Date + + """ + The name of the country or region where the maintainer resides. Will only + return a value when queried by the maintainer themselves, or by an admin of + the sponsorable organization. + """ + residenceCountryOrRegion: String + + """ + The HTTP path for this Sponsors listing. + """ + resourcePath: URI! + + """ + The short description of the listing. + """ + shortDescription: String! + + """ + The short name of the listing. + """ + slug: String! + + """ + The entity this listing represents who can be sponsored on GitHub Sponsors. + """ + sponsorable: Sponsorable! + + """ + The tiers for this GitHub Sponsors profile. + """ + tiers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether to include tiers that aren't published. Only admins of the Sponsors + listing can see draft tiers. Only admins of the Sponsors listing and viewers + who are currently sponsoring on a retired tier can see those retired tiers. + Defaults to including only published tiers, which are visible to anyone who + can see the GitHub Sponsors profile. + """ + includeUnpublished: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Sponsors tiers returned from the connection. + """ + orderBy: SponsorsTierOrder = {field: MONTHLY_PRICE_IN_CENTS, direction: ASC} + ): SponsorsTierConnection + + """ + The HTTP URL for this Sponsors listing. + """ + url: URI! +} + +""" +A record that can be featured on a GitHub Sponsors profile. +""" +union SponsorsListingFeatureableItem = Repository | User + +""" +A record that is promoted on a GitHub Sponsors profile. +""" +type SponsorsListingFeaturedItem implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Will either be a description from the sponsorable maintainer about why they + featured this item, or the item's description itself, such as a user's bio + from their GitHub profile page. + """ + description: String + + """ + The record that is featured on the GitHub Sponsors profile. + """ + featureable: SponsorsListingFeatureableItem! + id: ID! + + """ + The position of this featured item on the GitHub Sponsors profile with a lower + position indicating higher precedence. Starts at 1. + """ + position: Int! + + """ + The GitHub Sponsors profile that features this record. + """ + sponsorsListing: SponsorsListing! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The different kinds of records that can be featured on a GitHub Sponsors profile page. +""" +enum SponsorsListingFeaturedItemFeatureableType { + """ + A repository owned by the user or organization with the GitHub Sponsors profile. + """ + REPOSITORY + + """ + A user who belongs to the organization with the GitHub Sponsors profile. + """ + USER +} + +""" +A GitHub Sponsors tier associated with a GitHub Sponsors listing. +""" +type SponsorsTier implements Node { + """ + SponsorsTier information only visible to users that can administer the associated Sponsors listing. + """ + adminInfo: SponsorsTierAdminInfo + + """ + Get a different tier for this tier's maintainer that is at the same frequency + as this tier but with an equal or lesser cost. Returns the published tier with + the monthly price closest to this tier's without going over. + """ + closestLesserValueTier: SponsorsTier + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The description of the tier. + """ + description: String! + + """ + The tier description rendered to HTML + """ + descriptionHTML: HTML! + id: ID! + + """ + Whether this tier was chosen at checkout time by the sponsor rather than + defined ahead of time by the maintainer who manages the Sponsors listing. + """ + isCustomAmount: Boolean! + + """ + Whether this tier is only for use with one-time sponsorships. + """ + isOneTime: Boolean! + + """ + How much this tier costs per month in cents. + """ + monthlyPriceInCents: Int! + + """ + How much this tier costs per month in USD. + """ + monthlyPriceInDollars: Int! + + """ + The name of the tier. + """ + name: String! + + """ + The sponsors listing that this tier belongs to. + """ + sponsorsListing: SponsorsListing! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +SponsorsTier information only visible to users that can administer the associated Sponsors listing. +""" +type SponsorsTierAdminInfo { + """ + Indicates whether this tier is still a work in progress by the sponsorable and + not yet published to the associated GitHub Sponsors profile. Draft tiers + cannot be used for new sponsorships and will not be in use on existing + sponsorships. Draft tiers cannot be seen by anyone but the admins of the + GitHub Sponsors profile. + """ + isDraft: Boolean! + + """ + Indicates whether this tier is published to the associated GitHub Sponsors + profile. Published tiers are visible to anyone who can see the GitHub Sponsors + profile, and are available for use in sponsorships if the GitHub Sponsors + profile is publicly visible. + """ + isPublished: Boolean! + + """ + Indicates whether this tier has been retired from the associated GitHub + Sponsors profile. Retired tiers are no longer shown on the GitHub Sponsors + profile and cannot be chosen for new sponsorships. Existing sponsorships may + still use retired tiers if the sponsor selected the tier before it was retired. + """ + isRetired: Boolean! + + """ + The sponsorships using this tier. + """ + sponsorships( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether or not to return private sponsorships using this tier. Defaults to + only returning public sponsorships on this tier. + """ + includePrivate: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! +} + +""" +The connection type for SponsorsTier. +""" +type SponsorsTierConnection { + """ + A list of edges. + """ + edges: [SponsorsTierEdge] + + """ + A list of nodes. + """ + nodes: [SponsorsTier] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SponsorsTierEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SponsorsTier +} + +""" +Ordering options for Sponsors tiers connections. +""" +input SponsorsTierOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order tiers by. + """ + field: SponsorsTierOrderField! +} + +""" +Properties by which Sponsors tiers connections can be ordered. +""" +enum SponsorsTierOrderField { + """ + Order tiers by creation time. + """ + CREATED_AT + + """ + Order tiers by their monthly price in cents + """ + MONTHLY_PRICE_IN_CENTS +} + +""" +A sponsorship relationship between a sponsor and a maintainer +""" +type Sponsorship implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Whether the sponsorship is active. False implies the sponsor is a past sponsor + of the maintainer, while true implies they are a current sponsor. + """ + isActive: Boolean! + + """ + Whether this sponsorship represents a one-time payment versus a recurring sponsorship. + """ + isOneTimePayment: Boolean! + + """ + Whether the sponsor has chosen to receive sponsorship update emails sent from + the sponsorable. Only returns a non-null value when the viewer has permission to know this. + """ + isSponsorOptedIntoEmail: Boolean + + """ + The entity that is being sponsored + """ + maintainer: User! + @deprecated( + reason: "`Sponsorship.maintainer` will be removed. Use `Sponsorship.sponsorable` instead. Removal on 2020-04-01 UTC." + ) + + """ + The privacy level for this sponsorship. + """ + privacyLevel: SponsorshipPrivacy! + + """ + The user that is sponsoring. Returns null if the sponsorship is private or if sponsor is not a user. + """ + sponsor: User + @deprecated( + reason: "`Sponsorship.sponsor` will be removed. Use `Sponsorship.sponsorEntity` instead. Removal on 2020-10-01 UTC." + ) + + """ + The user or organization that is sponsoring, if you have permission to view them. + """ + sponsorEntity: Sponsor + + """ + The entity that is being sponsored + """ + sponsorable: Sponsorable! + + """ + The associated sponsorship tier + """ + tier: SponsorsTier + + """ + Identifies the date and time when the current tier was chosen for this sponsorship. + """ + tierSelectedAt: DateTime +} + +""" +The connection type for Sponsorship. +""" +type SponsorshipConnection { + """ + A list of edges. + """ + edges: [SponsorshipEdge] + + """ + A list of nodes. + """ + nodes: [Sponsorship] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + The total amount in cents of all recurring sponsorships in the connection + whose amount you can view. Does not include one-time sponsorships. + """ + totalRecurringMonthlyPriceInCents: Int! + + """ + The total amount in USD of all recurring sponsorships in the connection whose + amount you can view. Does not include one-time sponsorships. + """ + totalRecurringMonthlyPriceInDollars: Int! +} + +""" +An edge in a connection. +""" +type SponsorshipEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Sponsorship +} + +""" +An update sent to sponsors of a user or organization on GitHub Sponsors. +""" +type SponsorshipNewsletter implements Node { + """ + The author of the newsletter. + """ + author: User + + """ + The contents of the newsletter, the message the sponsorable wanted to give. + """ + body: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Indicates if the newsletter has been made available to sponsors. + """ + isPublished: Boolean! + + """ + The user or organization this newsletter is from. + """ + sponsorable: Sponsorable! + + """ + The subject of the newsletter, what it's about. + """ + subject: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for SponsorshipNewsletter. +""" +type SponsorshipNewsletterConnection { + """ + A list of edges. + """ + edges: [SponsorshipNewsletterEdge] + + """ + A list of nodes. + """ + nodes: [SponsorshipNewsletter] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SponsorshipNewsletterEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SponsorshipNewsletter +} + +""" +Ordering options for sponsorship newsletter connections. +""" +input SponsorshipNewsletterOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order sponsorship newsletters by. + """ + field: SponsorshipNewsletterOrderField! +} + +""" +Properties by which sponsorship update connections can be ordered. +""" +enum SponsorshipNewsletterOrderField { + """ + Order sponsorship newsletters by when they were created. + """ + CREATED_AT +} + +""" +Ordering options for sponsorship connections. +""" +input SponsorshipOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order sponsorship by. + """ + field: SponsorshipOrderField! +} + +""" +Properties by which sponsorship connections can be ordered. +""" +enum SponsorshipOrderField { + """ + Order sponsorship by creation time. + """ + CREATED_AT +} + +""" +The privacy of a sponsorship +""" +enum SponsorshipPrivacy { + """ + Private + """ + PRIVATE + + """ + Public + """ + PUBLIC +} + +""" +The possible default commit messages for squash merges. +""" +enum SquashMergeCommitMessage { + """ + Default to a blank commit message. + """ + BLANK + + """ + Default to the branch's commit messages. + """ + COMMIT_MESSAGES + + """ + Default to the pull request's body. + """ + PR_BODY +} + +""" +The possible default commit titles for squash merges. +""" +enum SquashMergeCommitTitle { + """ + Default to the commit's title (if only one commit) or the pull request's title (when more than one commit). + """ + COMMIT_OR_PR_TITLE + + """ + Default to the pull request's title. + """ + PR_TITLE +} + +""" +Represents an SSH signature on a Commit or Tag. +""" +type SshSignature implements GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Hex-encoded fingerprint of the key that signed this object. + """ + keyFingerprint: String + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +Ways in which star connections can be ordered. +""" +input StarOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field in which to order nodes by. + """ + field: StarOrderField! +} + +""" +Properties by which star connections can be ordered. +""" +enum StarOrderField { + """ + Allows ordering a list of stars by when they were created. + """ + STARRED_AT +} + +""" +The connection type for User. +""" +type StargazerConnection { + """ + A list of edges. + """ + edges: [StargazerEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user that's starred a repository. +""" +type StargazerEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: User! + + """ + Identifies when the item was starred. + """ + starredAt: DateTime! +} + +""" +Things that can be starred. +""" +interface Starrable { + id: ID! + + """ + Returns a count of how many stargazers there are on this object + """ + stargazerCount: Int! + + """ + A list of users who have starred this starrable. + """ + stargazers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + ): StargazerConnection! + + """ + Returns a boolean indicating whether the viewing user has starred this starrable. + """ + viewerHasStarred: Boolean! +} + +""" +The connection type for Repository. +""" +type StarredRepositoryConnection { + """ + A list of edges. + """ + edges: [StarredRepositoryEdge] + + """ + Is the list of stars for this user truncated? This is true for users that have many stars. + """ + isOverLimit: Boolean! + + """ + A list of nodes. + """ + nodes: [Repository] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a starred repository. +""" +type StarredRepositoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: Repository! + + """ + Identifies when the item was starred. + """ + starredAt: DateTime! +} + +""" +Autogenerated input type of StartOrganizationMigration +""" +input StartOrganizationMigrationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The migration source access token. + """ + sourceAccessToken: String! + + """ + The URL of the organization to migrate. + """ + sourceOrgUrl: URI! + + """ + The ID of the enterprise the target organization belongs to. + """ + targetEnterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The name of the target organization. + """ + targetOrgName: String! +} + +""" +Autogenerated return type of StartOrganizationMigration +""" +type StartOrganizationMigrationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new organization migration. + """ + orgMigration: OrganizationMigration +} + +""" +Autogenerated input type of StartRepositoryMigration +""" +input StartRepositoryMigrationInput { + """ + The migration source access token. + """ + accessToken: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether to continue the migration on error. Defaults to `false`. We strongly + recommend setting this to `true` for the smoothest migration experience. *This + default will change to `true` on September 4, 2023.* + """ + continueOnError: Boolean + + """ + The signed URL to access the user-uploaded git archive. + """ + gitArchiveUrl: String + + """ + The GitHub personal access token of the user importing to the target repository. + """ + githubPat: String + + """ + Whether to lock the source repository. + """ + lockSource: Boolean + + """ + The signed URL to access the user-uploaded metadata archive. + """ + metadataArchiveUrl: String + + """ + The ID of the organization that will own the imported repository. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + The name of the imported repository. + """ + repositoryName: String! + + """ + Whether to skip migrating releases for the repository. + """ + skipReleases: Boolean + + """ + The ID of the migration source. + """ + sourceId: ID! @possibleTypes(concreteTypes: ["MigrationSource"]) + + """ + The URL of the source repository. + """ + sourceRepositoryUrl: URI + + """ + The visibility of the imported repository. + """ + targetRepoVisibility: String +} + +""" +Autogenerated return type of StartRepositoryMigration +""" +type StartRepositoryMigrationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new repository migration. + """ + repositoryMigration: RepositoryMigration +} + +""" +Represents a commit status. +""" +type Status implements Node { + """ + A list of status contexts and check runs for this commit. + """ + combinedContexts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): StatusCheckRollupContextConnection! + + """ + The commit this status is attached to. + """ + commit: Commit + + """ + Looks up an individual status context by context name. + """ + context( + """ + The context name. + """ + name: String! + ): StatusContext + + """ + The individual status contexts for this commit. + """ + contexts: [StatusContext!]! + id: ID! + + """ + The combined commit status. + """ + state: StatusState! +} + +""" +Required status check +""" +type StatusCheckConfiguration { + """ + The status check context name that must be present on the commit. + """ + context: String! + + """ + The optional integration ID that this status check must originate from. + """ + integrationId: Int +} + +""" +Required status check +""" +input StatusCheckConfigurationInput { + """ + The status check context name that must be present on the commit. + """ + context: String! + + """ + The optional integration ID that this status check must originate from. + """ + integrationId: Int +} + +""" +Represents the rollup for both the check runs and status for a commit. +""" +type StatusCheckRollup implements Node { + """ + The commit the status and check runs are attached to. + """ + commit: Commit + + """ + A list of status contexts and check runs for this commit. + """ + contexts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): StatusCheckRollupContextConnection! + id: ID! + + """ + The combined status for the commit. + """ + state: StatusState! +} + +""" +Types that can be inside a StatusCheckRollup context. +""" +union StatusCheckRollupContext = CheckRun | StatusContext + +""" +The connection type for StatusCheckRollupContext. +""" +type StatusCheckRollupContextConnection { + """ + The number of check runs in this rollup. + """ + checkRunCount: Int! + + """ + Counts of check runs by state. + """ + checkRunCountsByState: [CheckRunStateCount!] + + """ + A list of edges. + """ + edges: [StatusCheckRollupContextEdge] + + """ + A list of nodes. + """ + nodes: [StatusCheckRollupContext] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The number of status contexts in this rollup. + """ + statusContextCount: Int! + + """ + Counts of status contexts by state. + """ + statusContextCountsByState: [StatusContextStateCount!] + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type StatusCheckRollupContextEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: StatusCheckRollupContext +} + +""" +Represents an individual commit status context +""" +type StatusContext implements Node & RequirableByPullRequest { + """ + The avatar of the OAuth application or the user that created the status + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int = 40 + ): URI + + """ + This commit this status context is attached to. + """ + commit: Commit + + """ + The name of this status context. + """ + context: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created this status context. + """ + creator: Actor + + """ + The description for this status context. + """ + description: String + id: ID! + + """ + Whether this is required to pass before merging for a specific pull request. + """ + isRequired( + """ + The id of the pull request this is required for + """ + pullRequestId: ID + + """ + The number of the pull request this is required for + """ + pullRequestNumber: Int + ): Boolean! + + """ + The state of this status context. + """ + state: StatusState! + + """ + The URL for this status context. + """ + targetUrl: URI +} + +""" +Represents a count of the state of a status context. +""" +type StatusContextStateCount { + """ + The number of statuses with this state. + """ + count: Int! + + """ + The state of a status context. + """ + state: StatusState! +} + +""" +The possible commit status states. +""" +enum StatusState { + """ + Status is errored. + """ + ERROR + + """ + Status is expected. + """ + EXPECTED + + """ + Status is failing. + """ + FAILURE + + """ + Status is pending. + """ + PENDING + + """ + Status is successful. + """ + SUCCESS +} + +""" +A Stripe Connect account for receiving sponsorship funds from GitHub Sponsors. +""" +type StripeConnectAccount { + """ + The account number used to identify this Stripe Connect account. + """ + accountId: String! + + """ + The name of the country or region of an external account, such as a bank + account, tied to the Stripe Connect account. Will only return a value when + queried by the maintainer of the associated GitHub Sponsors profile + themselves, or by an admin of the sponsorable organization. + """ + billingCountryOrRegion: String + + """ + The name of the country or region of the Stripe Connect account. Will only + return a value when queried by the maintainer of the associated GitHub + Sponsors profile themselves, or by an admin of the sponsorable organization. + """ + countryOrRegion: String + + """ + Whether this Stripe Connect account is currently in use for the associated GitHub Sponsors profile. + """ + isActive: Boolean! + + """ + The GitHub Sponsors profile associated with this Stripe Connect account. + """ + sponsorsListing: SponsorsListing! + + """ + The URL to access this Stripe Connect account on Stripe's website. + """ + stripeDashboardUrl: URI! +} + +""" +Autogenerated input type of SubmitPullRequestReview +""" +input SubmitPullRequestReviewInput { + """ + The text field to set on the Pull Request Review. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The event to send to the Pull Request Review. + """ + event: PullRequestReviewEvent! + + """ + The Pull Request ID to submit any pending reviews. + """ + pullRequestId: ID @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The Pull Request Review ID to submit. + """ + pullRequestReviewId: ID @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of SubmitPullRequestReview +""" +type SubmitPullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The submitted pull request review. + """ + pullRequestReview: PullRequestReview +} + +""" +A pointer to a repository at a specific revision embedded inside another repository. +""" +type Submodule { + """ + The branch of the upstream submodule for tracking updates + """ + branch: String + + """ + The git URL of the submodule repository + """ + gitUrl: URI! + + """ + The name of the submodule in .gitmodules + """ + name: String! + + """ + The name of the submodule in .gitmodules (Base64-encoded) + """ + nameRaw: Base64String! + + """ + The path in the superproject that this submodule is located in + """ + path: String! + + """ + The path in the superproject that this submodule is located in (Base64-encoded) + """ + pathRaw: Base64String! + + """ + The commit revision of the subproject repository being tracked by the submodule + """ + subprojectCommitOid: GitObjectID +} + +""" +The connection type for Submodule. +""" +type SubmoduleConnection { + """ + A list of edges. + """ + edges: [SubmoduleEdge] + + """ + A list of nodes. + """ + nodes: [Submodule] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SubmoduleEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Submodule +} + +""" +Entities that can be subscribed to for web and email notifications. +""" +interface Subscribable { + id: ID! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +Entities that can be subscribed to for web and email notifications. +""" +interface SubscribableThread { + id: ID! + + """ + Identifies the viewer's thread subscription form action. + """ + viewerThreadSubscriptionFormAction: ThreadSubscriptionFormAction + + """ + Identifies the viewer's thread subscription status. + """ + viewerThreadSubscriptionStatus: ThreadSubscriptionState +} + +""" +Represents a 'subscribed' event on a given `Subscribable`. +""" +type SubscribedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Object referenced by event. + """ + subscribable: Subscribable! +} + +""" +The possible states of a subscription. +""" +enum SubscriptionState { + """ + The User is never notified. + """ + IGNORED + + """ + The User is notified of all conversations. + """ + SUBSCRIBED + + """ + The User is only notified when participating or @mentioned. + """ + UNSUBSCRIBED +} + +""" +A suggestion to review a pull request based on a user's commit history and review comments. +""" +type SuggestedReviewer { + """ + Is this suggestion based on past commits? + """ + isAuthor: Boolean! + + """ + Is this suggestion based on past review comments? + """ + isCommenter: Boolean! + + """ + Identifies the user suggested to review the pull request. + """ + reviewer: User! +} + +""" +Represents a Git tag. +""" +type Tag implements GitObject & Node { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + id: ID! + + """ + The Git tag message. + """ + message: String + + """ + The Git tag name. + """ + name: String! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The Repository the Git object belongs to + """ + repository: Repository! + + """ + Details about the tag author. + """ + tagger: GitActor + + """ + The Git object the tag points to. + """ + target: GitObject! +} + +""" +Parameters to be used for the tag_name_pattern rule +""" +type TagNamePatternParameters { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean! + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Parameters to be used for the tag_name_pattern rule +""" +input TagNamePatternParametersInput { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +A team of users in an organization. +""" +type Team implements MemberStatusable & Node & Subscribable { + """ + A list of teams that are ancestors of this team. + """ + ancestors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): TeamConnection! + + """ + A URL pointing to the team's avatar. + """ + avatarUrl( + """ + The size in pixels of the resulting square image. + """ + size: Int = 400 + ): URI + + """ + List of child teams belonging to this team + """ + childTeams( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether to list immediate child teams or all descendant child teams. + """ + immediateOnly: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: TeamOrder + + """ + User logins to filter by + """ + userLogins: [String!] + ): TeamConnection! + + """ + The slug corresponding to the organization and team. + """ + combinedSlug: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The description of the team. + """ + description: String + + """ + Find a team discussion by its number. + """ + discussion( + """ + The sequence number of the discussion to find. + """ + number: Int! + ): TeamDiscussion + + """ + A list of team discussions. + """ + discussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If provided, filters discussions according to whether or not they are pinned. + """ + isPinned: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: TeamDiscussionOrder + ): TeamDiscussionConnection! + + """ + The HTTP path for team discussions + """ + discussionsResourcePath: URI! + + """ + The HTTP URL for team discussions + """ + discussionsUrl: URI! + + """ + The HTTP path for editing this team + """ + editTeamResourcePath: URI! + + """ + The HTTP URL for editing this team + """ + editTeamUrl: URI! + id: ID! + + """ + A list of pending invitations for users to this team + """ + invitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationInvitationConnection + + """ + Get the status messages members of this entity have set that are either public or visible only to the organization. + """ + memberStatuses( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for user statuses returned from the connection. + """ + orderBy: UserStatusOrder = {field: UPDATED_AT, direction: DESC} + ): UserStatusConnection! + + """ + A list of users who are members of this team. + """ + members( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter by membership type + """ + membership: TeamMembershipType = ALL + + """ + Order for the connection. + """ + orderBy: TeamMemberOrder + + """ + The search string to look for. + """ + query: String + + """ + Filter by team member role + """ + role: TeamMemberRole + ): TeamMemberConnection! + + """ + The HTTP path for the team' members + """ + membersResourcePath: URI! + + """ + The HTTP URL for the team' members + """ + membersUrl: URI! + + """ + The name of the team. + """ + name: String! + + """ + The HTTP path creating a new team + """ + newTeamResourcePath: URI! + + """ + The HTTP URL creating a new team + """ + newTeamUrl: URI! + + """ + The notification setting that the team has set. + """ + notificationSetting: TeamNotificationSetting! + + """ + The organization that owns this team. + """ + organization: Organization! + + """ + The parent team of the team. + """ + parentTeam: Team + + """ + The level of privacy the team has. + """ + privacy: TeamPrivacy! + + """ + Finds and returns the project according to the provided project number. + """ + projectV2( + """ + The Project number. + """ + number: Int! + ): ProjectV2 + + """ + List of projects this team has collaborator access to. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for projects returned from this connection + """ + filterBy: ProjectV2Filters = {} + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + The query to search projects by. + """ + query: String = "" + ): ProjectV2Connection! + + """ + A list of repositories this team has access to. + """ + repositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for the connection. + """ + orderBy: TeamRepositoryOrder + + """ + The search string to look for. Repositories will be returned where the name contains your search string. + """ + query: String + ): TeamRepositoryConnection! + + """ + The HTTP path for this team's repositories + """ + repositoriesResourcePath: URI! + + """ + The HTTP URL for this team's repositories + """ + repositoriesUrl: URI! + + """ + The HTTP path for this team + """ + resourcePath: URI! + + """ + What algorithm is used for review assignment for this team + """ + reviewRequestDelegationAlgorithm: TeamReviewAssignmentAlgorithm @preview(toggledBy: "stone-crop-preview") + + """ + True if review assignment is enabled for this team + """ + reviewRequestDelegationEnabled: Boolean! @preview(toggledBy: "stone-crop-preview") + + """ + How many team members are required for review assignment for this team + """ + reviewRequestDelegationMemberCount: Int @preview(toggledBy: "stone-crop-preview") + + """ + When assigning team members via delegation, whether the entire team should be notified as well. + """ + reviewRequestDelegationNotifyTeam: Boolean! @preview(toggledBy: "stone-crop-preview") + + """ + The slug corresponding to the team. + """ + slug: String! + + """ + The HTTP path for this team's teams + """ + teamsResourcePath: URI! + + """ + The HTTP URL for this team's teams + """ + teamsUrl: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this team + """ + url: URI! + + """ + Team is adminable by the viewer. + """ + viewerCanAdminister: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +Audit log entry for a team.add_member event. +""" +type TeamAddMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a team.add_repository event. +""" +type TeamAddRepositoryAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Metadata for an audit entry with action team.* +""" +interface TeamAuditEntryData { + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI +} + +""" +Audit log entry for a team.change_parent_team event. +""" +type TeamChangeParentTeamAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The new parent team. + """ + parentTeam: Team + + """ + The name of the new parent team + """ + parentTeamName: String + + """ + The name of the former parent team + """ + parentTeamNameWas: String + + """ + The HTTP path for the parent team + """ + parentTeamResourcePath: URI + + """ + The HTTP URL for the parent team + """ + parentTeamUrl: URI + + """ + The former parent team. + """ + parentTeamWas: Team + + """ + The HTTP path for the previous parent team + """ + parentTeamWasResourcePath: URI + + """ + The HTTP URL for the previous parent team + """ + parentTeamWasUrl: URI + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The connection type for Team. +""" +type TeamConnection { + """ + A list of edges. + """ + edges: [TeamEdge] + + """ + A list of nodes. + """ + nodes: [Team] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A team discussion. +""" +type TeamDiscussion implements Comment & Deletable & Node & Reactable & Subscribable & UniformResourceLocatable & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the discussion's team. + """ + authorAssociation: CommentAuthorAssociation! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the discussion body hash. + """ + bodyVersion: String! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + A list of comments on this discussion. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + When provided, filters the connection such that results begin with the comment with this number. + """ + fromComment: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: TeamDiscussionCommentOrder + ): TeamDiscussionCommentConnection! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The HTTP path for discussion comments + """ + commentsResourcePath: URI! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The HTTP URL for discussion comments + """ + commentsUrl: URI! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Whether or not the discussion is pinned. + """ + isPinned: Boolean! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Whether or not the discussion is only visible to team members and org admins. + """ + isPrivate: Boolean! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Identifies the discussion within its team. + """ + number: Int! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The HTTP path for this discussion + """ + resourcePath: URI! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The team that defines the context of this discussion. + """ + team: Team! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The title of the discussion + """ + title: String! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this discussion + """ + url: URI! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Whether or not the current viewer can pin this discussion. + """ + viewerCanPin: Boolean! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +A comment on a team discussion. +""" +type TeamDiscussionComment implements Comment & Deletable & Node & Reactable & UniformResourceLocatable & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the comment's team. + """ + authorAssociation: CommentAuthorAssociation! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + The current version of the body content. + """ + bodyVersion: String! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The discussion this comment is about. + """ + discussion: TeamDiscussion! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Identifies the comment number. + """ + number: Int! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The HTTP path for this comment + """ + resourcePath: URI! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this comment + """ + url: URI! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for TeamDiscussionComment. +""" +type TeamDiscussionCommentConnection { + """ + A list of edges. + """ + edges: [TeamDiscussionCommentEdge] + + """ + A list of nodes. + """ + nodes: [TeamDiscussionComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type TeamDiscussionCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: TeamDiscussionComment +} + +""" +Ways in which team discussion comment connections can be ordered. +""" +input TeamDiscussionCommentOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field by which to order nodes. + """ + field: TeamDiscussionCommentOrderField! +} + +""" +Properties by which team discussion comment connections can be ordered. +""" +enum TeamDiscussionCommentOrderField { + """ + Allows sequential ordering of team discussion comments (which is equivalent to chronological ordering). + """ + NUMBER +} + +""" +The connection type for TeamDiscussion. +""" +type TeamDiscussionConnection { + """ + A list of edges. + """ + edges: [TeamDiscussionEdge] + + """ + A list of nodes. + """ + nodes: [TeamDiscussion] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type TeamDiscussionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: TeamDiscussion +} + +""" +Ways in which team discussion connections can be ordered. +""" +input TeamDiscussionOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field by which to order nodes. + """ + field: TeamDiscussionOrderField! +} + +""" +Properties by which team discussion connections can be ordered. +""" +enum TeamDiscussionOrderField { + """ + Allows chronological ordering of team discussions. + """ + CREATED_AT +} + +""" +An edge in a connection. +""" +type TeamEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Team +} + +""" +The connection type for User. +""" +type TeamMemberConnection { + """ + A list of edges. + """ + edges: [TeamMemberEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user who is a member of a team. +""" +type TeamMemberEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The HTTP path to the organization's member access page. + """ + memberAccessResourcePath: URI! + + """ + The HTTP URL to the organization's member access page. + """ + memberAccessUrl: URI! + node: User! + + """ + The role the member has on the team. + """ + role: TeamMemberRole! +} + +""" +Ordering options for team member connections +""" +input TeamMemberOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order team members by. + """ + field: TeamMemberOrderField! +} + +""" +Properties by which team member connections can be ordered. +""" +enum TeamMemberOrderField { + """ + Order team members by creation time + """ + CREATED_AT + + """ + Order team members by login + """ + LOGIN +} + +""" +The possible team member roles; either 'maintainer' or 'member'. +""" +enum TeamMemberRole { + """ + A team maintainer has permission to add and remove team members. + """ + MAINTAINER + + """ + A team member has no administrative permissions on the team. + """ + MEMBER +} + +""" +Defines which types of team members are included in the returned list. Can be one of IMMEDIATE, CHILD_TEAM or ALL. +""" +enum TeamMembershipType { + """ + Includes immediate and child team members for the team. + """ + ALL + + """ + Includes only child team members for the team. + """ + CHILD_TEAM + + """ + Includes only immediate members of the team. + """ + IMMEDIATE +} + +""" +The possible team notification values. +""" +enum TeamNotificationSetting { + """ + No one will receive notifications. + """ + NOTIFICATIONS_DISABLED + + """ + Everyone will receive notifications when the team is @mentioned. + """ + NOTIFICATIONS_ENABLED +} + +""" +Ways in which team connections can be ordered. +""" +input TeamOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field in which to order nodes by. + """ + field: TeamOrderField! +} + +""" +Properties by which team connections can be ordered. +""" +enum TeamOrderField { + """ + Allows ordering a list of teams by name. + """ + NAME +} + +""" +The possible team privacy values. +""" +enum TeamPrivacy { + """ + A secret team can only be seen by its members. + """ + SECRET + + """ + A visible team can be seen and @mentioned by every member of the organization. + """ + VISIBLE +} + +""" +Audit log entry for a team.remove_member event. +""" +type TeamRemoveMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a team.remove_repository event. +""" +type TeamRemoveRepositoryAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The connection type for Repository. +""" +type TeamRepositoryConnection { + """ + A list of edges. + """ + edges: [TeamRepositoryEdge] + + """ + A list of nodes. + """ + nodes: [Repository] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a team repository. +""" +type TeamRepositoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: Repository! + + """ + The permission level the team has on the repository + """ + permission: RepositoryPermission! +} + +""" +Ordering options for team repository connections +""" +input TeamRepositoryOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repositories by. + """ + field: TeamRepositoryOrderField! +} + +""" +Properties by which team repository connections can be ordered. +""" +enum TeamRepositoryOrderField { + """ + Order repositories by creation time + """ + CREATED_AT + + """ + Order repositories by name + """ + NAME + + """ + Order repositories by permission + """ + PERMISSION + + """ + Order repositories by push time + """ + PUSHED_AT + + """ + Order repositories by number of stargazers + """ + STARGAZERS + + """ + Order repositories by update time + """ + UPDATED_AT +} + +""" +The possible team review assignment algorithms +""" +enum TeamReviewAssignmentAlgorithm @preview(toggledBy: "stone-crop-preview") { + """ + Balance review load across the entire team + """ + LOAD_BALANCE + + """ + Alternate reviews between each team member + """ + ROUND_ROBIN +} + +""" +The role of a user on a team. +""" +enum TeamRole { + """ + User has admin rights on the team. + """ + ADMIN + + """ + User is a member of the team. + """ + MEMBER +} + +""" +A text match within a search result. +""" +type TextMatch { + """ + The specific text fragment within the property matched on. + """ + fragment: String! + + """ + Highlights within the matched fragment. + """ + highlights: [TextMatchHighlight!]! + + """ + The property matched on. + """ + property: String! +} + +""" +Represents a single highlight in a search result match. +""" +type TextMatchHighlight { + """ + The indice in the fragment where the matched text begins. + """ + beginIndice: Int! + + """ + The indice in the fragment where the matched text ends. + """ + endIndice: Int! + + """ + The text matched. + """ + text: String! +} + +""" +The possible states of a thread subscription form action +""" +enum ThreadSubscriptionFormAction { + """ + The User cannot subscribe or unsubscribe to the thread + """ + NONE + + """ + The User can subscribe to the thread + """ + SUBSCRIBE + + """ + The User can unsubscribe to the thread + """ + UNSUBSCRIBE +} + +""" +The possible states of a subscription. +""" +enum ThreadSubscriptionState { + """ + The subscription status is currently disabled. + """ + DISABLED + + """ + The User is never notified because they are ignoring the list + """ + IGNORING_LIST + + """ + The User is never notified because they are ignoring the thread + """ + IGNORING_THREAD + + """ + The User is not recieving notifications from this thread + """ + NONE + + """ + The User is notified becuase they are watching the list + """ + SUBSCRIBED_TO_LIST + + """ + The User is notified because they are subscribed to the thread + """ + SUBSCRIBED_TO_THREAD + + """ + The User is notified because they chose custom settings for this thread. + """ + SUBSCRIBED_TO_THREAD_EVENTS + + """ + The User is notified because they chose custom settings for this thread. + """ + SUBSCRIBED_TO_THREAD_TYPE + + """ + The subscription status is currently unavailable. + """ + UNAVAILABLE +} + +""" +A topic aggregates entities that are related to a subject. +""" +type Topic implements Node & Starrable { + id: ID! + + """ + The topic's name. + """ + name: String! + + """ + A list of related topics, including aliases of this topic, sorted with the most relevant + first. Returns up to 10 Topics. + """ + relatedTopics( + """ + How many topics to return. + """ + first: Int = 3 + ): [Topic!]! + + """ + A list of repositories. + """ + repositories( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssuesEnabled: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + + """ + If true, only repositories whose owner can be sponsored via GitHub Sponsors will be returned. + """ + sponsorableOnly: Boolean = false + ): RepositoryConnection! + + """ + Returns a count of how many stargazers there are on this object + """ + stargazerCount: Int! + + """ + A list of users who have starred this starrable. + """ + stargazers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + ): StargazerConnection! + + """ + Returns a boolean indicating whether the viewing user has starred this starrable. + """ + viewerHasStarred: Boolean! +} + +""" +Metadata for an audit entry with a topic. +""" +interface TopicAuditEntryData { + """ + The name of the topic added to the repository + """ + topic: Topic + + """ + The name of the topic added to the repository + """ + topicName: String +} + +""" +Reason that the suggested topic is declined. +""" +enum TopicSuggestionDeclineReason { + """ + The suggested topic is not relevant to the repository. + """ + NOT_RELEVANT + + """ + The viewer does not like the suggested topic. + """ + PERSONAL_PREFERENCE + + """ + The suggested topic is too general for the repository. + """ + TOO_GENERAL + + """ + The suggested topic is too specific for the repository (e.g. #ruby-on-rails-version-4-2-1). + """ + TOO_SPECIFIC +} + +""" +The possible states of a tracked issue. +""" +enum TrackedIssueStates { + """ + The tracked issue is closed + """ + CLOSED + + """ + The tracked issue is open + """ + OPEN +} + +""" +Autogenerated input type of TransferEnterpriseOrganization +""" +input TransferEnterpriseOrganizationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise where the organization should be transferred. + """ + destinationEnterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The ID of the organization to transfer. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of TransferEnterpriseOrganization +""" +type TransferEnterpriseOrganizationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organization for which a transfer was initiated. + """ + organization: Organization +} + +""" +Autogenerated input type of TransferIssue +""" +input TransferIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether to create labels if they don't exist in the target repository (matched by name) + """ + createLabelsIfMissing: Boolean = false + + """ + The Node ID of the issue to be transferred + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + The Node ID of the repository the issue should be transferred to + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of TransferIssue +""" +type TransferIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was transferred + """ + issue: Issue +} + +""" +Represents a 'transferred' event on a given issue or pull request. +""" +type TransferredEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The repository this came from + """ + fromRepository: Repository + id: ID! + + """ + Identifies the issue associated with the event. + """ + issue: Issue! +} + +""" +Represents a Git tree. +""" +type Tree implements GitObject & Node { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + + """ + A list of tree entries. + """ + entries: [TreeEntry!] + id: ID! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The Repository the Git object belongs to + """ + repository: Repository! +} + +""" +Represents a Git tree entry. +""" +type TreeEntry { + """ + The extension of the file + """ + extension: String + + """ + Whether or not this tree entry is generated + """ + isGenerated: Boolean! + + """ + The programming language this file is written in. + """ + language: Language + + """ + Number of lines in the file. + """ + lineCount: Int + + """ + Entry file mode. + """ + mode: Int! + + """ + Entry file name. + """ + name: String! + + """ + Entry file name. (Base64-encoded) + """ + nameRaw: Base64String! + + """ + Entry file object. + """ + object: GitObject + + """ + Entry file Git object ID. + """ + oid: GitObjectID! + + """ + The full path of the file. + """ + path: String + + """ + The full path of the file. (Base64-encoded) + """ + pathRaw: Base64String + + """ + The Repository the tree entry belongs to + """ + repository: Repository! + + """ + Entry byte size + """ + size: Int! + + """ + If the TreeEntry is for a directory occupied by a submodule project, this returns the corresponding submodule + """ + submodule: Submodule + + """ + Entry file type. + """ + type: String! +} + +""" +An RFC 3986, RFC 3987, and RFC 6570 (level 4) compliant URI string. +""" +scalar URI + +""" +Autogenerated input type of UnarchiveProjectV2Item +""" +input UnarchiveProjectV2ItemInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the ProjectV2Item to unarchive. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the Project to archive the item from. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of UnarchiveProjectV2Item +""" +type UnarchiveProjectV2ItemPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item unarchived from the project. + """ + item: ProjectV2Item +} + +""" +Autogenerated input type of UnarchiveRepository +""" +input UnarchiveRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the repository to unarchive. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UnarchiveRepository +""" +type UnarchiveRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository that was unarchived. + """ + repository: Repository +} + +""" +Represents an 'unassigned' event on any assignable object. +""" +type UnassignedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the assignable associated with the event. + """ + assignable: Assignable! + + """ + Identifies the user or mannequin that was unassigned. + """ + assignee: Assignee + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the subject (user) who was unassigned. + """ + user: User + @deprecated(reason: "Assignees can now be mannequins. Use the `assignee` field instead. Removal on 2020-01-01 UTC.") +} + +""" +Autogenerated input type of UnfollowOrganization +""" +input UnfollowOrganizationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the organization to unfollow. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of UnfollowOrganization +""" +type UnfollowOrganizationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organization that was unfollowed. + """ + organization: Organization +} + +""" +Autogenerated input type of UnfollowUser +""" +input UnfollowUserInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the user to unfollow. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of UnfollowUser +""" +type UnfollowUserPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The user that was unfollowed. + """ + user: User +} + +""" +Represents a type that can be retrieved by a URL. +""" +interface UniformResourceLocatable { + """ + The HTML path to this resource. + """ + resourcePath: URI! + + """ + The URL to this resource. + """ + url: URI! +} + +""" +Represents an unknown signature on a Commit or Tag. +""" +type UnknownSignature implements GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +Represents an 'unlabeled' event on a given issue or pull request. +""" +type UnlabeledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the label associated with the 'unlabeled' event. + """ + label: Label! + + """ + Identifies the `Labelable` associated with the event. + """ + labelable: Labelable! +} + +""" +Autogenerated input type of UnlinkProjectV2FromRepository +""" +input UnlinkProjectV2FromRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the project to unlink from the repository. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The ID of the repository to unlink from the project. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UnlinkProjectV2FromRepository +""" +type UnlinkProjectV2FromRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository the project is no longer linked to. + """ + repository: Repository +} + +""" +Autogenerated input type of UnlinkProjectV2FromTeam +""" +input UnlinkProjectV2FromTeamInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the project to unlink from the team. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The ID of the team to unlink from the project. + """ + teamId: ID! @possibleTypes(concreteTypes: ["Team"]) +} + +""" +Autogenerated return type of UnlinkProjectV2FromTeam +""" +type UnlinkProjectV2FromTeamPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The team the project is unlinked from + """ + team: Team +} + +""" +Autogenerated input type of UnlinkRepositoryFromProject +""" +input UnlinkRepositoryFromProjectInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project linked to the Repository. + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) + + """ + The ID of the Repository linked to the Project. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UnlinkRepositoryFromProject +""" +type UnlinkRepositoryFromProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The linked Project. + """ + project: Project + + """ + The linked Repository. + """ + repository: Repository +} + +""" +Autogenerated input type of UnlockLockable +""" +input UnlockLockableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the item to be unlocked. + """ + lockableId: ID! @possibleTypes(concreteTypes: ["Discussion", "Issue", "PullRequest"], abstractType: "Lockable") +} + +""" +Autogenerated return type of UnlockLockable +""" +type UnlockLockablePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was unlocked. + """ + unlockedRecord: Lockable +} + +""" +Represents an 'unlocked' event on a given issue or pull request. +""" +type UnlockedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Object that was unlocked. + """ + lockable: Lockable! +} + +""" +Autogenerated input type of UnmarkDiscussionCommentAsAnswer +""" +input UnmarkDiscussionCommentAsAnswerInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion comment to unmark as an answer. + """ + id: ID! @possibleTypes(concreteTypes: ["DiscussionComment"]) +} + +""" +Autogenerated return type of UnmarkDiscussionCommentAsAnswer +""" +type UnmarkDiscussionCommentAsAnswerPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion that includes the comment. + """ + discussion: Discussion +} + +""" +Autogenerated input type of UnmarkFileAsViewed +""" +input UnmarkFileAsViewedInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The path of the file to mark as unviewed + """ + path: String! + + """ + The Node ID of the pull request. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of UnmarkFileAsViewed +""" +type UnmarkFileAsViewedPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of UnmarkIssueAsDuplicate +""" +input UnmarkIssueAsDuplicateInput { + """ + ID of the issue or pull request currently considered canonical/authoritative/original. + """ + canonicalId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "IssueOrPullRequest") + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the issue or pull request currently marked as a duplicate. + """ + duplicateId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "IssueOrPullRequest") +} + +""" +Autogenerated return type of UnmarkIssueAsDuplicate +""" +type UnmarkIssueAsDuplicatePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue or pull request that was marked as a duplicate. + """ + duplicate: IssueOrPullRequest +} + +""" +Autogenerated input type of UnmarkProjectV2AsTemplate +""" +input UnmarkProjectV2AsTemplateInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project to unmark as a template. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of UnmarkProjectV2AsTemplate +""" +type UnmarkProjectV2AsTemplatePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The project. + """ + projectV2: ProjectV2 +} + +""" +Represents an 'unmarked_as_duplicate' event on a given issue or pull request. +""" +type UnmarkedAsDuplicateEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + The authoritative issue or pull request which has been duplicated by another. + """ + canonical: IssueOrPullRequest + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The issue or pull request which has been marked as a duplicate of another. + """ + duplicate: IssueOrPullRequest + id: ID! + + """ + Canonical and duplicate belong to different repositories. + """ + isCrossRepository: Boolean! +} + +""" +Autogenerated input type of UnminimizeComment +""" +input UnminimizeCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! + @possibleTypes( + concreteTypes: ["CommitComment", "DiscussionComment", "GistComment", "IssueComment", "PullRequestReviewComment"] + abstractType: "Minimizable" + ) +} + +""" +Autogenerated return type of UnminimizeComment +""" +type UnminimizeCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The comment that was unminimized. + """ + unminimizedComment: Minimizable +} + +""" +Autogenerated input type of UnpinIssue +""" +input UnpinIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the issue to be unpinned + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of UnpinIssue +""" +type UnpinIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was unpinned + """ + issue: Issue +} + +""" +Represents an 'unpinned' event on a given issue or pull request. +""" +type UnpinnedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the issue associated with the event. + """ + issue: Issue! +} + +""" +Autogenerated input type of UnresolveReviewThread +""" +input UnresolveReviewThreadInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the thread to unresolve + """ + threadId: ID! @possibleTypes(concreteTypes: ["PullRequestReviewThread"]) +} + +""" +Autogenerated return type of UnresolveReviewThread +""" +type UnresolveReviewThreadPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The thread to resolve. + """ + thread: PullRequestReviewThread +} + +""" +Represents an 'unsubscribed' event on a given `Subscribable`. +""" +type UnsubscribedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Object referenced by event. + """ + subscribable: Subscribable! +} + +""" +Entities that can be updated. +""" +interface Updatable { + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! +} + +""" +Comments that can be updated. +""" +interface UpdatableComment { + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! +} + +""" +Autogenerated input type of UpdateBranchProtectionRule +""" +input UpdateBranchProtectionRuleInput { + """ + Can this branch be deleted. + """ + allowsDeletions: Boolean + + """ + Are force pushes allowed on this branch. + """ + allowsForcePushes: Boolean + + """ + Is branch creation a protected operation. + """ + blocksCreations: Boolean + + """ + The global relay id of the branch protection rule to be updated. + """ + branchProtectionRuleId: ID! @possibleTypes(concreteTypes: ["BranchProtectionRule"]) + + """ + A list of User, Team, or App IDs allowed to bypass force push targeting matching branches. + """ + bypassForcePushActorIds: [ID!] + + """ + A list of User, Team, or App IDs allowed to bypass pull requests targeting matching branches. + """ + bypassPullRequestActorIds: [ID!] + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Will new commits pushed to matching branches dismiss pull request review approvals. + """ + dismissesStaleReviews: Boolean + + """ + Can admins overwrite branch protection. + """ + isAdminEnforced: Boolean + + """ + Whether users can pull changes from upstream when the branch is locked. Set to + `true` to allow fork syncing. Set to `false` to prevent fork syncing. + """ + lockAllowsFetchAndMerge: Boolean + + """ + Whether to set the branch as read-only. If this is true, users will not be able to push to the branch. + """ + lockBranch: Boolean + + """ + The glob-like pattern used to determine matching branches. + """ + pattern: String + + """ + A list of User, Team, or App IDs allowed to push to matching branches. + """ + pushActorIds: [ID!] + + """ + Whether the most recent push must be approved by someone other than the person who pushed it + """ + requireLastPushApproval: Boolean + + """ + Number of approving reviews required to update matching branches. + """ + requiredApprovingReviewCount: Int + + """ + The list of required deployment environments + """ + requiredDeploymentEnvironments: [String!] + + """ + List of required status check contexts that must pass for commits to be accepted to matching branches. + """ + requiredStatusCheckContexts: [String!] + + """ + The list of required status checks + """ + requiredStatusChecks: [RequiredStatusCheckInput!] + + """ + Are approving reviews required to update matching branches. + """ + requiresApprovingReviews: Boolean + + """ + Are reviews from code owners required to update matching branches. + """ + requiresCodeOwnerReviews: Boolean + + """ + Are commits required to be signed. + """ + requiresCommitSignatures: Boolean + + """ + Are conversations required to be resolved before merging. + """ + requiresConversationResolution: Boolean + + """ + Are successful deployments required before merging. + """ + requiresDeployments: Boolean + + """ + Are merge commits prohibited from being pushed to this branch. + """ + requiresLinearHistory: Boolean + + """ + Are status checks required to update matching branches. + """ + requiresStatusChecks: Boolean + + """ + Are branches required to be up to date before merging. + """ + requiresStrictStatusChecks: Boolean + + """ + Is pushing to matching branches restricted. + """ + restrictsPushes: Boolean + + """ + Is dismissal of pull request reviews restricted. + """ + restrictsReviewDismissals: Boolean + + """ + A list of User, Team, or App IDs allowed to dismiss reviews on pull requests targeting matching branches. + """ + reviewDismissalActorIds: [ID!] +} + +""" +Autogenerated return type of UpdateBranchProtectionRule +""" +type UpdateBranchProtectionRulePayload { + """ + The newly created BranchProtectionRule. + """ + branchProtectionRule: BranchProtectionRule + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of UpdateCheckRun +""" +input UpdateCheckRunInput { + """ + Possible further actions the integrator can perform, which a user may trigger. + """ + actions: [CheckRunAction!] + + """ + The node of the check. + """ + checkRunId: ID! @possibleTypes(concreteTypes: ["CheckRun"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The time that the check run finished. + """ + completedAt: DateTime + + """ + The final conclusion of the check. + """ + conclusion: CheckConclusionState + + """ + The URL of the integrator's site that has the full details of the check. + """ + detailsUrl: URI + + """ + A reference for the run on the integrator's system. + """ + externalId: String + + """ + The name of the check. + """ + name: String + + """ + Descriptive details about the run. + """ + output: CheckRunOutput + + """ + The node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The time that the check run began. + """ + startedAt: DateTime + + """ + The current status. + """ + status: RequestableCheckStatusState +} + +""" +Autogenerated return type of UpdateCheckRun +""" +type UpdateCheckRunPayload { + """ + The updated check run. + """ + checkRun: CheckRun + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of UpdateCheckSuitePreferences +""" +input UpdateCheckSuitePreferencesInput { + """ + The check suite preferences to modify. + """ + autoTriggerPreferences: [CheckSuiteAutoTriggerPreference!]! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UpdateCheckSuitePreferences +""" +type UpdateCheckSuitePreferencesPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated repository. + """ + repository: Repository +} + +""" +Autogenerated input type of UpdateDiscussionComment +""" +input UpdateDiscussionCommentInput { + """ + The new contents of the comment body. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion comment to update. + """ + commentId: ID! @possibleTypes(concreteTypes: ["DiscussionComment"]) +} + +""" +Autogenerated return type of UpdateDiscussionComment +""" +type UpdateDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The modified discussion comment. + """ + comment: DiscussionComment +} + +""" +Autogenerated input type of UpdateDiscussion +""" +input UpdateDiscussionInput { + """ + The new contents of the discussion body. + """ + body: String + + """ + The Node ID of a discussion category within the same repository to change this discussion to. + """ + categoryId: ID @possibleTypes(concreteTypes: ["DiscussionCategory"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion to update. + """ + discussionId: ID! @possibleTypes(concreteTypes: ["Discussion"]) + + """ + The new discussion title. + """ + title: String +} + +""" +Autogenerated return type of UpdateDiscussion +""" +type UpdateDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The modified discussion. + """ + discussion: Discussion +} + +""" +Autogenerated input type of UpdateEnterpriseAdministratorRole +""" +input UpdateEnterpriseAdministratorRoleInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Enterprise which the admin belongs to. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a administrator whose role is being changed. + """ + login: String! + + """ + The new role for the Enterprise administrator. + """ + role: EnterpriseAdministratorRole! +} + +""" +Autogenerated return type of UpdateEnterpriseAdministratorRole +""" +type UpdateEnterpriseAdministratorRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of changing the administrator's role. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseAllowPrivateRepositoryForkingSetting +""" +input UpdateEnterpriseAllowPrivateRepositoryForkingSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the allow private repository forking setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the allow private repository forking policy on the enterprise. + """ + policyValue: EnterpriseAllowPrivateRepositoryForkingPolicyValue + + """ + The value for the allow private repository forking setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseAllowPrivateRepositoryForkingSetting +""" +type UpdateEnterpriseAllowPrivateRepositoryForkingSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated allow private repository forking setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the allow private repository forking setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseDefaultRepositoryPermissionSetting +""" +input UpdateEnterpriseDefaultRepositoryPermissionSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the base repository permission setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the base repository permission setting on the enterprise. + """ + settingValue: EnterpriseDefaultRepositoryPermissionSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseDefaultRepositoryPermissionSetting +""" +type UpdateEnterpriseDefaultRepositoryPermissionSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated base repository permission setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the base repository permission setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanChangeRepositoryVisibilitySetting +""" +input UpdateEnterpriseMembersCanChangeRepositoryVisibilitySettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can change repository visibility setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can change repository visibility setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanChangeRepositoryVisibilitySetting +""" +type UpdateEnterpriseMembersCanChangeRepositoryVisibilitySettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can change repository visibility setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can change repository visibility setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanCreateRepositoriesSetting +""" +input UpdateEnterpriseMembersCanCreateRepositoriesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can create repositories setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + Allow members to create internal repositories. Defaults to current value. + """ + membersCanCreateInternalRepositories: Boolean + + """ + Allow members to create private repositories. Defaults to current value. + """ + membersCanCreatePrivateRepositories: Boolean + + """ + Allow members to create public repositories. Defaults to current value. + """ + membersCanCreatePublicRepositories: Boolean + + """ + When false, allow member organizations to set their own repository creation member privileges. + """ + membersCanCreateRepositoriesPolicyEnabled: Boolean + + """ + Value for the members can create repositories setting on the enterprise. This + or the granular public/private/internal allowed fields (but not both) must be provided. + """ + settingValue: EnterpriseMembersCanCreateRepositoriesSettingValue +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanCreateRepositoriesSetting +""" +type UpdateEnterpriseMembersCanCreateRepositoriesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can create repositories setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can create repositories setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanDeleteIssuesSetting +""" +input UpdateEnterpriseMembersCanDeleteIssuesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can delete issues setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can delete issues setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanDeleteIssuesSetting +""" +type UpdateEnterpriseMembersCanDeleteIssuesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can delete issues setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can delete issues setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanDeleteRepositoriesSetting +""" +input UpdateEnterpriseMembersCanDeleteRepositoriesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can delete repositories setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can delete repositories setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanDeleteRepositoriesSetting +""" +type UpdateEnterpriseMembersCanDeleteRepositoriesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can delete repositories setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can delete repositories setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanInviteCollaboratorsSetting +""" +input UpdateEnterpriseMembersCanInviteCollaboratorsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can invite collaborators setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can invite collaborators setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanInviteCollaboratorsSetting +""" +type UpdateEnterpriseMembersCanInviteCollaboratorsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can invite collaborators setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can invite collaborators setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanMakePurchasesSetting +""" +input UpdateEnterpriseMembersCanMakePurchasesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can make purchases setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can make purchases setting on the enterprise. + """ + settingValue: EnterpriseMembersCanMakePurchasesSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanMakePurchasesSetting +""" +type UpdateEnterpriseMembersCanMakePurchasesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can make purchases setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can make purchases setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanUpdateProtectedBranchesSetting +""" +input UpdateEnterpriseMembersCanUpdateProtectedBranchesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can update protected branches setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can update protected branches setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanUpdateProtectedBranchesSetting +""" +type UpdateEnterpriseMembersCanUpdateProtectedBranchesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can update protected branches setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can update protected branches setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanViewDependencyInsightsSetting +""" +input UpdateEnterpriseMembersCanViewDependencyInsightsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can view dependency insights setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can view dependency insights setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanViewDependencyInsightsSetting +""" +type UpdateEnterpriseMembersCanViewDependencyInsightsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can view dependency insights setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can view dependency insights setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseOrganizationProjectsSetting +""" +input UpdateEnterpriseOrganizationProjectsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the organization projects setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the organization projects setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseOrganizationProjectsSetting +""" +type UpdateEnterpriseOrganizationProjectsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated organization projects setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the organization projects setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseOwnerOrganizationRole +""" +input UpdateEnterpriseOwnerOrganizationRoleInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Enterprise which the owner belongs to. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The ID of the organization for membership change. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + The role to assume in the organization. + """ + organizationRole: RoleInOrganization! +} + +""" +Autogenerated return type of UpdateEnterpriseOwnerOrganizationRole +""" +type UpdateEnterpriseOwnerOrganizationRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of changing the owner's organization role. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseProfile +""" +input UpdateEnterpriseProfileInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The description of the enterprise. + """ + description: String + + """ + The Enterprise ID to update. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The location of the enterprise. + """ + location: String + + """ + The name of the enterprise. + """ + name: String + + """ + The URL of the enterprise's website. + """ + websiteUrl: String +} + +""" +Autogenerated return type of UpdateEnterpriseProfile +""" +type UpdateEnterpriseProfilePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated enterprise. + """ + enterprise: Enterprise +} + +""" +Autogenerated input type of UpdateEnterpriseRepositoryProjectsSetting +""" +input UpdateEnterpriseRepositoryProjectsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the repository projects setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the repository projects setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseRepositoryProjectsSetting +""" +type UpdateEnterpriseRepositoryProjectsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated repository projects setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the repository projects setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseTeamDiscussionsSetting +""" +input UpdateEnterpriseTeamDiscussionsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the team discussions setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the team discussions setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseTeamDiscussionsSetting +""" +type UpdateEnterpriseTeamDiscussionsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated team discussions setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the team discussions setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseTwoFactorAuthenticationRequiredSetting +""" +input UpdateEnterpriseTwoFactorAuthenticationRequiredSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the two factor authentication required setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the two factor authentication required setting on the enterprise. + """ + settingValue: EnterpriseEnabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseTwoFactorAuthenticationRequiredSetting +""" +type UpdateEnterpriseTwoFactorAuthenticationRequiredSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated two factor authentication required setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the two factor authentication required setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnvironment +""" +input UpdateEnvironmentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The node ID of the environment. + """ + environmentId: ID! @possibleTypes(concreteTypes: ["Environment"]) + + """ + The ids of users or teams that can approve deployments to this environment + """ + reviewers: [ID!] + + """ + The wait timer in minutes. + """ + waitTimer: Int +} + +""" +Autogenerated return type of UpdateEnvironment +""" +type UpdateEnvironmentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated environment. + """ + environment: Environment +} + +""" +Autogenerated input type of UpdateIpAllowListEnabledSetting +""" +input UpdateIpAllowListEnabledSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the owner on which to set the IP allow list enabled setting. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["App", "Enterprise", "Organization"], abstractType: "IpAllowListOwner") + + """ + The value for the IP allow list enabled setting. + """ + settingValue: IpAllowListEnabledSettingValue! +} + +""" +Autogenerated return type of UpdateIpAllowListEnabledSetting +""" +type UpdateIpAllowListEnabledSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list owner on which the setting was updated. + """ + owner: IpAllowListOwner +} + +""" +Autogenerated input type of UpdateIpAllowListEntry +""" +input UpdateIpAllowListEntryInput { + """ + An IP address or range of addresses in CIDR notation. + """ + allowListValue: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the IP allow list entry to update. + """ + ipAllowListEntryId: ID! @possibleTypes(concreteTypes: ["IpAllowListEntry"]) + + """ + Whether the IP allow list entry is active when an IP allow list is enabled. + """ + isActive: Boolean! + + """ + An optional name for the IP allow list entry. + """ + name: String +} + +""" +Autogenerated return type of UpdateIpAllowListEntry +""" +type UpdateIpAllowListEntryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list entry that was updated. + """ + ipAllowListEntry: IpAllowListEntry +} + +""" +Autogenerated input type of UpdateIpAllowListForInstalledAppsEnabledSetting +""" +input UpdateIpAllowListForInstalledAppsEnabledSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the owner. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["App", "Enterprise", "Organization"], abstractType: "IpAllowListOwner") + + """ + The value for the IP allow list configuration for installed GitHub Apps setting. + """ + settingValue: IpAllowListForInstalledAppsEnabledSettingValue! +} + +""" +Autogenerated return type of UpdateIpAllowListForInstalledAppsEnabledSetting +""" +type UpdateIpAllowListForInstalledAppsEnabledSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list owner on which the setting was updated. + """ + owner: IpAllowListOwner +} + +""" +Autogenerated input type of UpdateIssueComment +""" +input UpdateIssueCommentInput { + """ + The updated text of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the IssueComment to modify. + """ + id: ID! @possibleTypes(concreteTypes: ["IssueComment"]) +} + +""" +Autogenerated return type of UpdateIssueComment +""" +type UpdateIssueCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated comment. + """ + issueComment: IssueComment +} + +""" +Autogenerated input type of UpdateIssue +""" +input UpdateIssueInput { + """ + An array of Node IDs of users for this issue. + """ + assigneeIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The body for the issue description. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Issue to modify. + """ + id: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + An array of Node IDs of labels for this issue. + """ + labelIds: [ID!] @possibleTypes(concreteTypes: ["Label"]) + + """ + The Node ID of the milestone for this issue. + """ + milestoneId: ID @possibleTypes(concreteTypes: ["Milestone"]) + + """ + An array of Node IDs for projects associated with this issue. + """ + projectIds: [ID!] + + """ + The desired issue state. + """ + state: IssueState + + """ + The title for the issue. + """ + title: String +} + +""" +Autogenerated return type of UpdateIssue +""" +type UpdateIssuePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue. + """ + issue: Issue +} + +""" +Autogenerated input type of UpdateLabel +""" +input UpdateLabelInput @preview(toggledBy: "bane-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A 6 character hex code, without the leading #, identifying the updated color of the label. + """ + color: String + + """ + A brief description of the label, such as its purpose. + """ + description: String + + """ + The Node ID of the label to be updated. + """ + id: ID! @possibleTypes(concreteTypes: ["Label"]) + + """ + The updated name of the label. + """ + name: String +} + +""" +Autogenerated return type of UpdateLabel +""" +type UpdateLabelPayload @preview(toggledBy: "bane-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated label. + """ + label: Label +} + +""" +Autogenerated input type of UpdateNotificationRestrictionSetting +""" +input UpdateNotificationRestrictionSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the owner on which to set the restrict notifications setting. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Enterprise", "Organization"], abstractType: "VerifiableDomainOwner") + + """ + The value for the restrict notifications setting. + """ + settingValue: NotificationRestrictionSettingValue! +} + +""" +Autogenerated return type of UpdateNotificationRestrictionSetting +""" +type UpdateNotificationRestrictionSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The owner on which the setting was updated. + """ + owner: VerifiableDomainOwner +} + +""" +Autogenerated input type of UpdateOrganizationAllowPrivateRepositoryForkingSetting +""" +input UpdateOrganizationAllowPrivateRepositoryForkingSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Enable forking of private repositories in the organization? + """ + forkingEnabled: Boolean! + + """ + The ID of the organization on which to set the allow private repository forking setting. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of UpdateOrganizationAllowPrivateRepositoryForkingSetting +""" +type UpdateOrganizationAllowPrivateRepositoryForkingSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of updating the allow private repository forking setting. + """ + message: String + + """ + The organization with the updated allow private repository forking setting. + """ + organization: Organization +} + +""" +Autogenerated input type of UpdateOrganizationWebCommitSignoffSetting +""" +input UpdateOrganizationWebCommitSignoffSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the organization on which to set the web commit signoff setting. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + Enable signoff on web-based commits for repositories in the organization? + """ + webCommitSignoffRequired: Boolean! +} + +""" +Autogenerated return type of UpdateOrganizationWebCommitSignoffSetting +""" +type UpdateOrganizationWebCommitSignoffSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of updating the web commit signoff setting. + """ + message: String + + """ + The organization with the updated web commit signoff setting. + """ + organization: Organization +} + +""" +Only allow users with bypass permission to update matching refs. +""" +type UpdateParameters { + """ + Branch can pull changes from its upstream repository + """ + updateAllowsFetchAndMerge: Boolean! +} + +""" +Only allow users with bypass permission to update matching refs. +""" +input UpdateParametersInput { + """ + Branch can pull changes from its upstream repository + """ + updateAllowsFetchAndMerge: Boolean! +} + +""" +Autogenerated input type of UpdateProjectCard +""" +input UpdateProjectCardInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether or not the ProjectCard should be archived + """ + isArchived: Boolean + + """ + The note of ProjectCard. + """ + note: String + + """ + The ProjectCard ID to update. + """ + projectCardId: ID! @possibleTypes(concreteTypes: ["ProjectCard"]) +} + +""" +Autogenerated return type of UpdateProjectCard +""" +type UpdateProjectCardPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated ProjectCard. + """ + projectCard: ProjectCard +} + +""" +Autogenerated input type of UpdateProjectColumn +""" +input UpdateProjectColumnInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of project column. + """ + name: String! + + """ + The ProjectColumn ID to update. + """ + projectColumnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of UpdateProjectColumn +""" +type UpdateProjectColumnPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated project column. + """ + projectColumn: ProjectColumn +} + +""" +Autogenerated input type of UpdateProject +""" +input UpdateProjectInput { + """ + The description of project. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of project. + """ + name: String + + """ + The Project ID to update. + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) + + """ + Whether the project is public or not. + """ + public: Boolean + + """ + Whether the project is open or closed. + """ + state: ProjectState +} + +""" +Autogenerated return type of UpdateProject +""" +type UpdateProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated project. + """ + project: Project +} + +""" +Autogenerated input type of UpdateProjectV2Collaborators +""" +input UpdateProjectV2CollaboratorsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The collaborators to update. + """ + collaborators: [ProjectV2Collaborator!]! + + """ + The ID of the project to update the collaborators for. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of UpdateProjectV2Collaborators +""" +type UpdateProjectV2CollaboratorsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The collaborators granted a role + """ + collaborators( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2ActorConnection +} + +""" +Autogenerated input type of UpdateProjectV2DraftIssue +""" +input UpdateProjectV2DraftIssueInput { + """ + The IDs of the assignees of the draft issue. + """ + assigneeIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The body of the draft issue. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the draft issue to update. + """ + draftIssueId: ID! @possibleTypes(concreteTypes: ["DraftIssue"]) + + """ + The title of the draft issue. + """ + title: String +} + +""" +Autogenerated return type of UpdateProjectV2DraftIssue +""" +type UpdateProjectV2DraftIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The draft issue updated in the project. + """ + draftIssue: DraftIssue +} + +""" +Autogenerated input type of UpdateProjectV2 +""" +input UpdateProjectV2Input { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Set the project to closed or open. + """ + closed: Boolean + + """ + The ID of the Project to update. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + Set the project to public or private. + """ + public: Boolean + + """ + Set the readme description of the project. + """ + readme: String + + """ + Set the short description of the project. + """ + shortDescription: String + + """ + Set the title of the project. + """ + title: String +} + +""" +Autogenerated input type of UpdateProjectV2ItemFieldValue +""" +input UpdateProjectV2ItemFieldValueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the field to be updated. + """ + fieldId: ID! + @possibleTypes( + concreteTypes: ["ProjectV2Field", "ProjectV2IterationField", "ProjectV2SingleSelectField"] + abstractType: "ProjectV2FieldConfiguration" + ) + + """ + The ID of the item to be updated. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the Project. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The value which will be set on the field. + """ + value: ProjectV2FieldValue! +} + +""" +Autogenerated return type of UpdateProjectV2ItemFieldValue +""" +type UpdateProjectV2ItemFieldValuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated item. + """ + projectV2Item: ProjectV2Item +} + +""" +Autogenerated input type of UpdateProjectV2ItemPosition +""" +input UpdateProjectV2ItemPositionInput { + """ + The ID of the item to position this item after. If omitted or set to null the item will be moved to top. + """ + afterId: ID @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the item to be moved. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the Project. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of UpdateProjectV2ItemPosition +""" +type UpdateProjectV2ItemPositionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The items in the new order + """ + items( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2ItemConnection +} + +""" +Autogenerated return type of UpdateProjectV2 +""" +type UpdateProjectV2Payload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated Project. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of UpdatePullRequestBranch +""" +input UpdatePullRequestBranchInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The head ref oid for the upstream branch. + """ + expectedHeadOid: GitObjectID + + """ + The Node ID of the pull request. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The update branch method to use. If omitted, defaults to 'MERGE' + """ + updateMethod: PullRequestBranchUpdateMethod +} + +""" +Autogenerated return type of UpdatePullRequestBranch +""" +type UpdatePullRequestBranchPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of UpdatePullRequest +""" +input UpdatePullRequestInput { + """ + An array of Node IDs of users for this pull request. + """ + assigneeIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The name of the branch you want your changes pulled into. This should be an existing branch + on the current repository. + """ + baseRefName: String + + """ + The contents of the pull request. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + An array of Node IDs of labels for this pull request. + """ + labelIds: [ID!] @possibleTypes(concreteTypes: ["Label"]) + + """ + Indicates whether maintainers can modify the pull request. + """ + maintainerCanModify: Boolean + + """ + The Node ID of the milestone for this pull request. + """ + milestoneId: ID @possibleTypes(concreteTypes: ["Milestone"]) + + """ + An array of Node IDs for projects associated with this pull request. + """ + projectIds: [ID!] + + """ + The Node ID of the pull request. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The target state of the pull request. + """ + state: PullRequestUpdateState + + """ + The title of the pull request. + """ + title: String +} + +""" +Autogenerated return type of UpdatePullRequest +""" +type UpdatePullRequestPayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of UpdatePullRequestReviewComment +""" +input UpdatePullRequestReviewCommentInput { + """ + The text of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the comment to modify. + """ + pullRequestReviewCommentId: ID! @possibleTypes(concreteTypes: ["PullRequestReviewComment"]) +} + +""" +Autogenerated return type of UpdatePullRequestReviewComment +""" +type UpdatePullRequestReviewCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated comment. + """ + pullRequestReviewComment: PullRequestReviewComment +} + +""" +Autogenerated input type of UpdatePullRequestReview +""" +input UpdatePullRequestReviewInput { + """ + The contents of the pull request review body. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pull request review to modify. + """ + pullRequestReviewId: ID! @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of UpdatePullRequestReview +""" +type UpdatePullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request review. + """ + pullRequestReview: PullRequestReview +} + +""" +Autogenerated input type of UpdateRef +""" +input UpdateRefInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Permit updates of branch Refs that are not fast-forwards? + """ + force: Boolean = false + + """ + The GitObjectID that the Ref shall be updated to target. + """ + oid: GitObjectID! + + """ + The Node ID of the Ref to be updated. + """ + refId: ID! @possibleTypes(concreteTypes: ["Ref"]) +} + +""" +Autogenerated return type of UpdateRef +""" +type UpdateRefPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated Ref. + """ + ref: Ref +} + +""" +Autogenerated input type of UpdateRefs +""" +input UpdateRefsInput @preview(toggledBy: "update-refs-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A list of ref updates. + """ + refUpdates: [RefUpdate!]! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UpdateRefs +""" +type UpdateRefsPayload @preview(toggledBy: "update-refs-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of UpdateRepository +""" +input UpdateRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A new description for the repository. Pass an empty string to erase the existing description. + """ + description: String + + """ + Indicates if the repository should have the discussions feature enabled. + """ + hasDiscussionsEnabled: Boolean + + """ + Indicates if the repository should have the issues feature enabled. + """ + hasIssuesEnabled: Boolean + + """ + Indicates if the repository should have the project boards feature enabled. + """ + hasProjectsEnabled: Boolean + + """ + Indicates if the repository should have the wiki feature enabled. + """ + hasWikiEnabled: Boolean + + """ + The URL for a web page about this repository. Pass an empty string to erase the existing URL. + """ + homepageUrl: URI + + """ + The new name of the repository. + """ + name: String + + """ + The ID of the repository to update. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + Whether this repository should be marked as a template such that anyone who + can access it can create new repositories with the same files and directory structure. + """ + template: Boolean +} + +""" +Autogenerated return type of UpdateRepository +""" +type UpdateRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated repository. + """ + repository: Repository +} + +""" +Autogenerated input type of UpdateRepositoryRuleset +""" +input UpdateRepositoryRulesetInput { + """ + A list of actors that are allowed to bypass rules in this ruleset. + """ + bypassActors: [RepositoryRulesetBypassActorInput!] + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The list of conditions for this ruleset + """ + conditions: RepositoryRuleConditionsInput + + """ + The enforcement level for this ruleset + """ + enforcement: RuleEnforcement + + """ + The name of the ruleset. + """ + name: String + + """ + The global relay id of the repository ruleset to be updated. + """ + repositoryRulesetId: ID! @possibleTypes(concreteTypes: ["RepositoryRuleset"]) + + """ + The list of rules for this ruleset + """ + rules: [RepositoryRuleInput!] + + """ + The target of the ruleset. + """ + target: RepositoryRulesetTarget +} + +""" +Autogenerated return type of UpdateRepositoryRuleset +""" +type UpdateRepositoryRulesetPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created Ruleset. + """ + ruleset: RepositoryRuleset +} + +""" +Autogenerated input type of UpdateRepositoryWebCommitSignoffSetting +""" +input UpdateRepositoryWebCommitSignoffSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the repository to update. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + Indicates if the repository should require signoff on web-based commits. + """ + webCommitSignoffRequired: Boolean! +} + +""" +Autogenerated return type of UpdateRepositoryWebCommitSignoffSetting +""" +type UpdateRepositoryWebCommitSignoffSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of updating the web commit signoff setting. + """ + message: String + + """ + The updated repository. + """ + repository: Repository +} + +""" +Autogenerated input type of UpdateSponsorshipPreferences +""" +input UpdateSponsorshipPreferencesInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Specify whether others should be able to see that the sponsor is sponsoring + the sponsorable. Public visibility still does not reveal which tier is used. + """ + privacyLevel: SponsorshipPrivacy = PUBLIC + + """ + Whether the sponsor should receive email updates from the sponsorable. + """ + receiveEmails: Boolean = true + + """ + The ID of the user or organization who is acting as the sponsor, paying for + the sponsorship. Required if sponsorLogin is not given. + """ + sponsorId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsor") + + """ + The username of the user or organization who is acting as the sponsor, paying + for the sponsorship. Required if sponsorId is not given. + """ + sponsorLogin: String + + """ + The ID of the user or organization who is receiving the sponsorship. Required if sponsorableLogin is not given. + """ + sponsorableId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsorable") + + """ + The username of the user or organization who is receiving the sponsorship. Required if sponsorableId is not given. + """ + sponsorableLogin: String +} + +""" +Autogenerated return type of UpdateSponsorshipPreferences +""" +type UpdateSponsorshipPreferencesPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The sponsorship that was updated. + """ + sponsorship: Sponsorship +} + +""" +Autogenerated input type of UpdateSubscription +""" +input UpdateSubscriptionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new state of the subscription. + """ + state: SubscriptionState! + + """ + The Node ID of the subscribable object to modify. + """ + subscribableId: ID! + @possibleTypes( + concreteTypes: ["Commit", "Discussion", "Issue", "PullRequest", "Repository", "Team", "TeamDiscussion"] + abstractType: "Subscribable" + ) +} + +""" +Autogenerated return type of UpdateSubscription +""" +type UpdateSubscriptionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The input subscribable entity. + """ + subscribable: Subscribable +} + +""" +Autogenerated input type of UpdateTeamDiscussionComment +""" +input UpdateTeamDiscussionCommentInput { + """ + The updated text of the comment. + """ + body: String! + + """ + The current version of the body content. + """ + bodyVersion: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the comment to modify. + """ + id: ID! @possibleTypes(concreteTypes: ["TeamDiscussionComment"]) +} + +""" +Autogenerated return type of UpdateTeamDiscussionComment +""" +type UpdateTeamDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated comment. + """ + teamDiscussionComment: TeamDiscussionComment +} + +""" +Autogenerated input type of UpdateTeamDiscussion +""" +input UpdateTeamDiscussionInput { + """ + The updated text of the discussion. + """ + body: String + + """ + The current version of the body content. If provided, this update operation + will be rejected if the given version does not match the latest version on the server. + """ + bodyVersion: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion to modify. + """ + id: ID! @possibleTypes(concreteTypes: ["TeamDiscussion"]) + + """ + If provided, sets the pinned state of the updated discussion. + """ + pinned: Boolean + + """ + The updated title of the discussion. + """ + title: String +} + +""" +Autogenerated return type of UpdateTeamDiscussion +""" +type UpdateTeamDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated discussion. + """ + teamDiscussion: TeamDiscussion +} + +""" +Autogenerated input type of UpdateTeamReviewAssignment +""" +input UpdateTeamReviewAssignmentInput @preview(toggledBy: "stone-crop-preview") { + """ + The algorithm to use for review assignment + """ + algorithm: TeamReviewAssignmentAlgorithm = ROUND_ROBIN + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Turn on or off review assignment + """ + enabled: Boolean! + + """ + An array of team member IDs to exclude + """ + excludedTeamMemberIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The Node ID of the team to update review assignments of + """ + id: ID! @possibleTypes(concreteTypes: ["Team"]) + + """ + Notify the entire team of the PR if it is delegated + """ + notifyTeam: Boolean = true + + """ + The number of team members to assign + """ + teamMemberCount: Int = 1 +} + +""" +Autogenerated return type of UpdateTeamReviewAssignment +""" +type UpdateTeamReviewAssignmentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The team that was modified + """ + team: Team +} + +""" +Autogenerated input type of UpdateTeamsRepository +""" +input UpdateTeamsRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Permission that should be granted to the teams. + """ + permission: RepositoryPermission! + + """ + Repository ID being granted access to. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + A list of teams being granted access. Limit: 10 + """ + teamIds: [ID!]! @possibleTypes(concreteTypes: ["Team"]) +} + +""" +Autogenerated return type of UpdateTeamsRepository +""" +type UpdateTeamsRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository that was updated. + """ + repository: Repository + + """ + The teams granted permission on the repository. + """ + teams: [Team!] +} + +""" +Autogenerated input type of UpdateTopics +""" +input UpdateTopicsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + An array of topic names. + """ + topicNames: [String!]! +} + +""" +Autogenerated return type of UpdateTopics +""" +type UpdateTopicsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Names of the provided topics that are not valid. + """ + invalidTopicNames: [String!] + + """ + The updated repository. + """ + repository: Repository +} + +""" +A user is an individual's account on GitHub that owns repositories and can make new content. +""" +type User implements Actor & Node & PackageOwner & ProfileOwner & ProjectOwner & ProjectV2Owner & ProjectV2Recent & RepositoryDiscussionAuthor & RepositoryDiscussionCommentAuthor & RepositoryOwner & Sponsorable & UniformResourceLocatable { + """ + Determine if this repository owner has any items that can be pinned to their profile. + """ + anyPinnableItems( + """ + Filter to only a particular kind of pinnable item. + """ + type: PinnableItemType + ): Boolean! + + """ + A URL pointing to the user's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The user's public profile bio. + """ + bio: String + + """ + The user's public profile bio as HTML. + """ + bioHTML: HTML! + + """ + Could this user receive email notifications, if the organization had notification restrictions enabled? + """ + canReceiveOrganizationEmailsWhenNotificationsRestricted( + """ + The login of the organization to check. + """ + login: String! + ): Boolean! + + """ + A list of commit comments made by this user. + """ + commitComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + The user's public profile company. + """ + company: String + + """ + The user's public profile company as HTML. + """ + companyHTML: HTML! + + """ + The collection of contributions this user has made to different repositories. + """ + contributionsCollection( + """ + Only contributions made at this time or later will be counted. If omitted, defaults to a year ago. + """ + from: DateTime + + """ + The ID of the organization used to filter contributions. + """ + organizationID: ID + + """ + Only contributions made before and up to (including) this time will be + counted. If omitted, defaults to the current time or one year from the + provided from argument. + """ + to: DateTime + ): ContributionsCollection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The user's publicly visible profile email. + """ + email: String! + + """ + The estimated next GitHub Sponsors payout for this user/organization in cents (USD). + """ + estimatedNextSponsorsPayoutInCents: Int! + + """ + A list of users the given user is followed by. + """ + followers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): FollowerConnection! + + """ + A list of users the given user is following. + """ + following( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): FollowingConnection! + + """ + Find gist by repo name. + """ + gist( + """ + The gist name to find. + """ + name: String! + ): Gist + + """ + A list of gist comments made by this user. + """ + gistComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): GistCommentConnection! + + """ + A list of the Gists the user has created. + """ + gists( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for gists returned from the connection + """ + orderBy: GistOrder + + """ + Filters Gists according to privacy. + """ + privacy: GistPrivacy + ): GistConnection! + + """ + True if this user/organization has a GitHub Sponsors listing. + """ + hasSponsorsListing: Boolean! + + """ + The hovercard information for this user in a given context + """ + hovercard( + """ + The ID of the subject to get the hovercard in the context of + """ + primarySubjectId: ID + ): Hovercard! + id: ID! + + """ + The interaction ability settings for this user. + """ + interactionAbility: RepositoryInteractionAbility + + """ + Whether or not this user is a participant in the GitHub Security Bug Bounty. + """ + isBountyHunter: Boolean! + + """ + Whether or not this user is a participant in the GitHub Campus Experts Program. + """ + isCampusExpert: Boolean! + + """ + Whether or not this user is a GitHub Developer Program member. + """ + isDeveloperProgramMember: Boolean! + + """ + Whether or not this user is a GitHub employee. + """ + isEmployee: Boolean! + + """ + Whether or not this user is following the viewer. Inverse of viewerIsFollowing + """ + isFollowingViewer: Boolean! + + """ + Whether or not this user is a member of the GitHub Stars Program. + """ + isGitHubStar: Boolean! + + """ + Whether or not the user has marked themselves as for hire. + """ + isHireable: Boolean! + + """ + Whether or not this user is a site administrator. + """ + isSiteAdmin: Boolean! + + """ + Whether the given account is sponsoring this user/organization. + """ + isSponsoredBy( + """ + The target account's login. + """ + accountLogin: String! + ): Boolean! + + """ + True if the viewer is sponsored by this user/organization. + """ + isSponsoringViewer: Boolean! + + """ + Whether or not this user is the viewing user. + """ + isViewer: Boolean! + + """ + A list of issue comments made by this user. + """ + issueComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issue comments returned from the connection. + """ + orderBy: IssueCommentOrder + ): IssueCommentConnection! + + """ + A list of issues associated with this user. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + Showcases a selection of repositories and gists that the profile owner has + either curated or that have been selected automatically based on popularity. + """ + itemShowcase: ProfileItemShowcase! + + """ + The user's public profile location. + """ + location: String + + """ + The username used to login. + """ + login: String! + + """ + The estimated monthly GitHub Sponsors income for this user/organization in cents (USD). + """ + monthlyEstimatedSponsorsIncomeInCents: Int! + + """ + The user's public profile name. + """ + name: String + + """ + Find an organization by its login that the user belongs to. + """ + organization( + """ + The login of the organization to find. + """ + login: String! + ): Organization + + """ + Verified email addresses that match verified domains for a specified organization the user is a member of. + """ + organizationVerifiedDomainEmails( + """ + The login of the organization to match verified domains from. + """ + login: String! + ): [String!]! + + """ + A list of organizations the user belongs to. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the User's organizations. + """ + orderBy: OrganizationOrder = null + ): OrganizationConnection! + + """ + A list of packages under the owner. + """ + packages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Find packages by their names. + """ + names: [String] + + """ + Ordering of the returned packages. + """ + orderBy: PackageOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter registry package by type. + """ + packageType: PackageType + + """ + Find packages in a repository by ID. + """ + repositoryId: ID + ): PackageConnection! + + """ + A list of repositories and gists this profile owner can pin to their profile. + """ + pinnableItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinnable items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + A list of repositories and gists this profile owner has pinned to their profile + """ + pinnedItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinned items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + Returns how many more items this profile owner can pin to their profile. + """ + pinnedItemsRemaining: Int! + + """ + Find project by number. + """ + project( + """ + The project number to find. + """ + number: Int! + ): Project + + """ + Find a project by number. + """ + projectV2( + """ + The project number. + """ + number: Int! + ): ProjectV2 + + """ + A list of projects under the owner. + """ + projects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for projects returned from the connection + """ + orderBy: ProjectOrder + + """ + Query to search projects by, currently only searching by name. + """ + search: String + + """ + A list of states to filter the projects by. + """ + states: [ProjectState!] + ): ProjectConnection! + + """ + The HTTP path listing user's projects + """ + projectsResourcePath: URI! + + """ + The HTTP URL listing user's projects + """ + projectsUrl: URI! + + """ + A list of projects under the owner. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + A project to search for under the the owner. + """ + query: String + ): ProjectV2Connection! + + """ + The user's profile pronouns + """ + pronouns: String + + """ + A list of public keys associated with this user. + """ + publicKeys( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PublicKeyConnection! + + """ + A list of pull requests associated with this user. + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + Recent projects that this user has modified in the context of the owner. + """ + recentProjects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2Connection! + + """ + A list of repositories that the user owns. + """ + repositories( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssuesEnabled: Boolean + + """ + If non-null, filters repositories according to whether they are archived and not maintained + """ + isArchived: Boolean + + """ + If non-null, filters repositories according to whether they are forks of another repository + """ + isFork: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + A list of repositories that the user recently contributed to. + """ + repositoriesContributedTo( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + If non-null, include only the specified types of contributions. The + GitHub.com UI uses [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY] + """ + contributionTypes: [RepositoryContributionType] + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssues: Boolean + + """ + If true, include user repositories + """ + includeUserRepositories: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + Find Repository. + """ + repository( + """ + Follow repository renames. If disabled, a repository referenced by its old name will return an error. + """ + followRenames: Boolean = true + + """ + Name of Repository to find. + """ + name: String! + ): Repository + + """ + Discussion comments this user has authored. + """ + repositoryDiscussionComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter discussion comments to only those that were marked as the answer + """ + onlyAnswers: Boolean = false + + """ + Filter discussion comments to only those in a specific repository. + """ + repositoryId: ID + ): DiscussionCommentConnection! + + """ + Discussions this user has started. + """ + repositoryDiscussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Filter discussions to only those that have been answered or not. Defaults to + including both answered and unanswered discussions. + """ + answered: Boolean = null + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for discussions returned from the connection. + """ + orderBy: DiscussionOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter discussions to only those in a specific repository. + """ + repositoryId: ID + + """ + A list of states to filter the discussions by. + """ + states: [DiscussionState!] = [] + ): DiscussionConnection! + + """ + The HTTP path for this user + """ + resourcePath: URI! + + """ + Replies this user has saved + """ + savedReplies( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The field to order saved replies by. + """ + orderBy: SavedReplyOrder = {field: UPDATED_AT, direction: DESC} + ): SavedReplyConnection + + """ + The user's social media accounts, ordered as they appear on the user's profile. + """ + socialAccounts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): SocialAccountConnection! + + """ + List of users and organizations this entity is sponsoring. + """ + sponsoring( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the users and organizations returned from the connection. + """ + orderBy: SponsorOrder = {field: RELEVANCE, direction: DESC} + ): SponsorConnection! + + """ + List of sponsors for this user or organization. + """ + sponsors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsors returned from the connection. + """ + orderBy: SponsorOrder = {field: RELEVANCE, direction: DESC} + + """ + If given, will filter for sponsors at the given tier. Will only return + sponsors whose tier the viewer is permitted to see. + """ + tierId: ID + ): SponsorConnection! + + """ + Events involving this sponsorable, such as new sponsorships. + """ + sponsorsActivities( + """ + Filter activities to only the specified actions. + """ + actions: [SponsorsActivityAction!] = [] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether to include those events where this sponsorable acted as the sponsor. + Defaults to only including events where this sponsorable was the recipient + of a sponsorship. + """ + includeAsSponsor: Boolean = false + + """ + Whether or not to include private activities in the result set. Defaults to including public and private activities. + """ + includePrivate: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for activity returned from the connection. + """ + orderBy: SponsorsActivityOrder = {field: TIMESTAMP, direction: DESC} + + """ + Filter activities returned to only those that occurred in the most recent + specified time period. Set to ALL to avoid filtering by when the activity + occurred. Will be ignored if `since` or `until` is given. + """ + period: SponsorsActivityPeriod = MONTH + + """ + Filter activities to those that occurred on or after this time. + """ + since: DateTime + + """ + Filter activities to those that occurred before this time. + """ + until: DateTime + ): SponsorsActivityConnection! + + """ + The GitHub Sponsors listing for this user or organization. + """ + sponsorsListing: SponsorsListing + + """ + The sponsorship from the viewer to this user/organization; that is, the sponsorship where you're the sponsor. + """ + sponsorshipForViewerAsSponsor( + """ + Whether to return the sponsorship only if it's still active. Pass false to + get the viewer's sponsorship back even if it has been cancelled. + """ + activeOnly: Boolean = true + ): Sponsorship + + """ + The sponsorship from this user/organization to the viewer; that is, the sponsorship you're receiving. + """ + sponsorshipForViewerAsSponsorable( + """ + Whether to return the sponsorship only if it's still active. Pass false to + get the sponsorship back even if it has been cancelled. + """ + activeOnly: Boolean = true + ): Sponsorship + + """ + List of sponsorship updates sent from this sponsorable to sponsors. + """ + sponsorshipNewsletters( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorship updates returned from the connection. + """ + orderBy: SponsorshipNewsletterOrder = {field: CREATED_AT, direction: DESC} + ): SponsorshipNewsletterConnection! + + """ + The sponsorships where this user or organization is the maintainer receiving the funds. + """ + sponsorshipsAsMaintainer( + """ + Whether to include only sponsorships that are active right now, versus all + sponsorships this maintainer has ever received. + """ + activeOnly: Boolean = true + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether or not to include private sponsorships in the result set + """ + includePrivate: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + The sponsorships where this user or organization is the funder. + """ + sponsorshipsAsSponsor( + """ + Whether to include only sponsorships that are active right now, versus all sponsorships this sponsor has ever made. + """ + activeOnly: Boolean = true + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter sponsorships returned to those for the specified maintainers. That + is, the recipient of the sponsorship is a user or organization with one of + the given logins. + """ + maintainerLogins: [String!] + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + Repositories the user has starred. + """ + starredRepositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + + """ + Filters starred repositories to only return repositories owned by the viewer. + """ + ownedByViewer: Boolean + ): StarredRepositoryConnection! + + """ + The user's description of what they're currently doing. + """ + status: UserStatus + + """ + Repositories the user has contributed to, ordered by contribution rank, plus repositories the user has created + """ + topRepositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder! + + """ + How far back in time to fetch contributed repositories + """ + since: DateTime + ): RepositoryConnection! + + """ + The amount in United States cents (e.g., 500 = $5.00 USD) that this entity has + spent on GitHub to fund sponsorships. Only returns a value when viewed by the + user themselves or by a user who can manage sponsorships for the requested organization. + """ + totalSponsorshipAmountAsSponsorInCents( + """ + Filter payments to those that occurred on or after this time. + """ + since: DateTime + + """ + Filter payments to those made to the users or organizations with the specified usernames. + """ + sponsorableLogins: [String!] = [] + + """ + Filter payments to those that occurred before this time. + """ + until: DateTime + ): Int + + """ + The user's Twitter username. + """ + twitterUsername: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this user + """ + url: URI! + + """ + Can the viewer pin repositories and gists to the profile? + """ + viewerCanChangePinnedItems: Boolean! + + """ + Can the current viewer create new projects on this owner. + """ + viewerCanCreateProjects: Boolean! + + """ + Whether or not the viewer is able to follow the user. + """ + viewerCanFollow: Boolean! + + """ + Whether or not the viewer is able to sponsor this user/organization. + """ + viewerCanSponsor: Boolean! + + """ + Whether or not this user is followed by the viewer. Inverse of isFollowingViewer. + """ + viewerIsFollowing: Boolean! + + """ + True if the viewer is sponsoring this user/organization. + """ + viewerIsSponsoring: Boolean! + + """ + A list of repositories the given user is watching. + """ + watching( + """ + Affiliation options for repositories returned from the connection. If none + specified, the results will include repositories for which the current + viewer is an owner or collaborator, or member. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssuesEnabled: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + A URL pointing to the user's public website/blog. + """ + websiteUrl: URI +} + +""" +The possible durations that a user can be blocked for. +""" +enum UserBlockDuration { + """ + The user was blocked for 1 day + """ + ONE_DAY + + """ + The user was blocked for 30 days + """ + ONE_MONTH + + """ + The user was blocked for 7 days + """ + ONE_WEEK + + """ + The user was blocked permanently + """ + PERMANENT + + """ + The user was blocked for 3 days + """ + THREE_DAYS +} + +""" +Represents a 'user_blocked' event on a given user. +""" +type UserBlockedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Number of days that the user was blocked for. + """ + blockDuration: UserBlockDuration! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + The user who was blocked. + """ + subject: User +} + +""" +The connection type for User. +""" +type UserConnection { + """ + A list of edges. + """ + edges: [UserEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edit on user content +""" +type UserContentEdit implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the date and time when the object was deleted. + """ + deletedAt: DateTime + + """ + The actor who deleted this content + """ + deletedBy: Actor + + """ + A summary of the changes for this edit + """ + diff: String + + """ + When this content was edited + """ + editedAt: DateTime! + + """ + The actor who edited this content + """ + editor: Actor + id: ID! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +A list of edits to content. +""" +type UserContentEditConnection { + """ + A list of edges. + """ + edges: [UserContentEditEdge] + + """ + A list of nodes. + """ + nodes: [UserContentEdit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type UserContentEditEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: UserContentEdit +} + +""" +Represents a user. +""" +type UserEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: User +} + +""" +Email attributes from External Identity +""" +type UserEmailMetadata { + """ + Boolean to identify primary emails + """ + primary: Boolean + + """ + Type of email + """ + type: String + + """ + Email id + """ + value: String! +} + +""" +The user's description of what they're currently doing. +""" +type UserStatus implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + An emoji summarizing the user's status. + """ + emoji: String + + """ + The status emoji as HTML. + """ + emojiHTML: HTML + + """ + If set, the status will not be shown after this date. + """ + expiresAt: DateTime + id: ID! + + """ + Whether this status indicates the user is not fully available on GitHub. + """ + indicatesLimitedAvailability: Boolean! + + """ + A brief message describing what the user is doing. + """ + message: String + + """ + The organization whose members can see this status. If null, this status is publicly visible. + """ + organization: Organization + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The user who has this status. + """ + user: User! +} + +""" +The connection type for UserStatus. +""" +type UserStatusConnection { + """ + A list of edges. + """ + edges: [UserStatusEdge] + + """ + A list of nodes. + """ + nodes: [UserStatus] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type UserStatusEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: UserStatus +} + +""" +Ordering options for user status connections. +""" +input UserStatusOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order user statuses by. + """ + field: UserStatusOrderField! +} + +""" +Properties by which user status connections can be ordered. +""" +enum UserStatusOrderField { + """ + Order user statuses by when they were updated. + """ + UPDATED_AT +} + +""" +A domain that can be verified or approved for an organization or an enterprise. +""" +type VerifiableDomain implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The DNS host name that should be used for verification. + """ + dnsHostName: URI + + """ + The unicode encoded domain. + """ + domain: URI! + + """ + Whether a TXT record for verification with the expected host name was found. + """ + hasFoundHostName: Boolean! + + """ + Whether a TXT record for verification with the expected verification token was found. + """ + hasFoundVerificationToken: Boolean! + id: ID! + + """ + Whether or not the domain is approved. + """ + isApproved: Boolean! + + """ + Whether this domain is required to exist for an organization or enterprise policy to be enforced. + """ + isRequiredForPolicyEnforcement: Boolean! + + """ + Whether or not the domain is verified. + """ + isVerified: Boolean! + + """ + The owner of the domain. + """ + owner: VerifiableDomainOwner! + + """ + The punycode encoded domain. + """ + punycodeEncodedDomain: URI! + + """ + The time that the current verification token will expire. + """ + tokenExpirationTime: DateTime + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The current verification token for the domain. + """ + verificationToken: String +} + +""" +The connection type for VerifiableDomain. +""" +type VerifiableDomainConnection { + """ + A list of edges. + """ + edges: [VerifiableDomainEdge] + + """ + A list of nodes. + """ + nodes: [VerifiableDomain] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type VerifiableDomainEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: VerifiableDomain +} + +""" +Ordering options for verifiable domain connections. +""" +input VerifiableDomainOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order verifiable domains by. + """ + field: VerifiableDomainOrderField! +} + +""" +Properties by which verifiable domain connections can be ordered. +""" +enum VerifiableDomainOrderField { + """ + Order verifiable domains by their creation date. + """ + CREATED_AT + + """ + Order verifiable domains by the domain name. + """ + DOMAIN +} + +""" +Types that can own a verifiable domain. +""" +union VerifiableDomainOwner = Enterprise | Organization + +""" +Autogenerated input type of VerifyVerifiableDomain +""" +input VerifyVerifiableDomainInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the verifiable domain to verify. + """ + id: ID! @possibleTypes(concreteTypes: ["VerifiableDomain"]) +} + +""" +Autogenerated return type of VerifyVerifiableDomain +""" +type VerifyVerifiableDomainPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The verifiable domain that was verified. + """ + domain: VerifiableDomain +} + +""" +A hovercard context with a message describing how the viewer is related. +""" +type ViewerHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! + + """ + Identifies the user who is related to this context. + """ + viewer: User! +} + +""" +A subject that may be upvoted. +""" +interface Votable { + """ + Number of upvotes that this subject has received. + """ + upvoteCount: Int! + + """ + Whether or not the current user can add or remove an upvote on this subject. + """ + viewerCanUpvote: Boolean! + + """ + Whether or not the current user has already upvoted this subject. + """ + viewerHasUpvoted: Boolean! +} + +""" +A workflow contains meta information about an Actions workflow file. +""" +type Workflow implements Node & UniformResourceLocatable { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The name of the workflow. + """ + name: String! + + """ + The HTTP path for this workflow + """ + resourcePath: URI! + + """ + The runs of the workflow. + """ + runs( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the connection + """ + orderBy: WorkflowRunOrder = {field: CREATED_AT, direction: DESC} + ): WorkflowRunConnection! + + """ + The state of the workflow. + """ + state: WorkflowState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this workflow + """ + url: URI! +} + +""" +A workflow run. +""" +type WorkflowRun implements Node & UniformResourceLocatable { + """ + The check suite this workflow run belongs to. + """ + checkSuite: CheckSuite! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The log of deployment reviews + """ + deploymentReviews( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentReviewConnection! + + """ + The event that triggered the workflow run + """ + event: String! + + """ + The workflow file + """ + file: WorkflowRunFile + id: ID! + + """ + The pending deployment requests of all check runs in this workflow run + """ + pendingDeploymentRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentRequestConnection! + + """ + The HTTP path for this workflow run + """ + resourcePath: URI! + + """ + A number that uniquely identifies this workflow run in its parent workflow. + """ + runNumber: Int! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this workflow run + """ + url: URI! + + """ + The workflow executed in this workflow run. + """ + workflow: Workflow! +} + +""" +The connection type for WorkflowRun. +""" +type WorkflowRunConnection { + """ + A list of edges. + """ + edges: [WorkflowRunEdge] + + """ + A list of nodes. + """ + nodes: [WorkflowRun] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type WorkflowRunEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: WorkflowRun +} + +""" +An executed workflow file for a workflow run. +""" +type WorkflowRunFile implements Node & UniformResourceLocatable { + id: ID! + + """ + The path of the workflow file relative to its repository. + """ + path: String! + + """ + The direct link to the file in the repository which stores the workflow file. + """ + repositoryFileUrl: URI! + + """ + The repository name and owner which stores the workflow file. + """ + repositoryName: URI! + + """ + The HTTP path for this workflow run file + """ + resourcePath: URI! + + """ + The parent workflow run execution for this file. + """ + run: WorkflowRun! + + """ + The HTTP URL for this workflow run file + """ + url: URI! + + """ + If the viewer has permissions to push to the repository which stores the workflow. + """ + viewerCanPushRepository: Boolean! + + """ + If the viewer has permissions to read the repository which stores the workflow. + """ + viewerCanReadRepository: Boolean! +} + +""" +Ways in which lists of workflow runs can be ordered upon return. +""" +input WorkflowRunOrder { + """ + The direction in which to order workflow runs by the specified field. + """ + direction: OrderDirection! + + """ + The field by which to order workflows. + """ + field: WorkflowRunOrderField! +} + +""" +Properties by which workflow run connections can be ordered. +""" +enum WorkflowRunOrderField { + """ + Order workflow runs by most recently created + """ + CREATED_AT +} + +""" +The possible states for a workflow. +""" +enum WorkflowState { + """ + The workflow is active. + """ + ACTIVE + + """ + The workflow was deleted from the git repository. + """ + DELETED + + """ + The workflow was disabled by default on a fork. + """ + DISABLED_FORK + + """ + The workflow was disabled for inactivity in the repository. + """ + DISABLED_INACTIVITY + + """ + The workflow was disabled manually. + """ + DISABLED_MANUALLY +} + +""" +A valid x509 certificate string +""" +scalar X509Certificate diff --git a/services/github/github/.eslintrc.js b/services/github/github/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/github/github/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/github/github/.npmignore b/services/github/github/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/github/github/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/github/github/config/rig.json b/services/github/github/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/github/github/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/github/github/jest.config.js b/services/github/github/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/github/github/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/github/github/package.json b/services/github/github/package.json new file mode 100644 index 0000000000..d059292064 --- /dev/null +++ b/services/github/github/package.json @@ -0,0 +1,53 @@ +{ + "name": "@hcengineering/github", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5" + }, + "dependencies": { + "@hcengineering/core": "^0.6.32", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/ui": "^0.6.15", + "@hcengineering/view": "^0.6.13", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/chunter": "^0.6.20", + "@hcengineering/attachment": "^0.6.14", + "@hcengineering/task": "^0.6.20", + "@hcengineering/tracker": "^0.6.24", + "@hcengineering/tags": "^0.6.16", + "@hcengineering/preference": "^0.6.13", + "@hcengineering/activity": "^0.6.0", + "@hcengineering/time": "^0.6.0" + } +} diff --git a/services/github/github/src/index.ts b/services/github/github/src/index.ts new file mode 100644 index 0000000000..557b83a4d8 --- /dev/null +++ b/services/github/github/src/index.ts @@ -0,0 +1,605 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +import { ActivityMessage, ActivityMessageViewlet } from '@hcengineering/activity' +import { Attachment } from '@hcengineering/attachment' +import { Person } from '@hcengineering/contact' +import { + Account, + AnyAttribute, + AttachedDoc, + Class, + Data, + Doc, + Hyperlink, + Markup, + Mixin, + Ref, + Timestamp +} from '@hcengineering/core' +import { Asset, IntlString, Metadata, Plugin, plugin } from '@hcengineering/platform' +import { Preference } from '@hcengineering/preference' +import task, { ProjectTypeDescriptor, TaskStatusFactory, TaskTypeDescriptor } from '@hcengineering/task' +import { ToDo } from '@hcengineering/time' +import { Component, Issue, Milestone, Project } from '@hcengineering/tracker' +import { AnyComponent } from '@hcengineering/ui' +import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' + +/** + * @public + * + * Define information about bi-directional synchronization with some other source. + */ +export interface DocSyncInfo extends Doc { + // _id === objectId + url: string + objectClass: Ref> + + lastGithubUser?: Ref | null + + // If repository is null, then document is not yet synced. + repository: Ref | null + external?: any // a raw data from external source, it should be mapped to platform document. + externalVersion?: string + externalCheckId?: string + + // Will contain derived state if required. + derivedVersion?: string + + current?: any // a current document being synchronized. + needSync: string // Will be updated by server trigger, in case of document changes of particular kind. + githubNumber: number + + lastModified?: number + + // Parent url + parent?: string + + // If set, issue is marked as deleted. + deleted?: boolean + + // In case of error will retry in certain amount of time. + error?: any + + [string: string]: any +} + +/** + * @public + */ +export interface GithubRepositoryRef { + name: string + id: string + prefix: string +} + +/** + * @public + */ +export interface GithubPullRequestFileReview { + fileName: string + sha: string +} + +/** + * @public + * + * Mixin with extra stuff for issues/pull requests related to github. + */ +export interface GithubIssue extends Issue { + // Github URL of the issue or Pull request + url: Hyperlink + githubNumber: number + repository: Ref + + // In case specified, description will be locked on editing until Allow edit button will be clicked on top bar. + // Button will also show a diff between a real data and updated markdown. + descriptionLocked?: boolean +} + +/** + * @public + * + * Mixin with extra stuff for todo's + */ +export interface GithubTodo extends ToDo { + // Github URL of the issue or Pull request + purpose: 'review' | 'fix' + + threadIds?: string[] +} + +/** + * @public + * + * Mixin to mark person as github enabled + */ +export interface GithubUser extends Person { + url: Hyperlink +} + +/** + * @public + * + * Mixin to manage github component to repository mapping. + */ +export interface GithubComponent extends Component { + repository: Ref + // If true, component represent repository and could not be removed/edited. + represent?: boolean +} + +/** + * @public + */ +export enum PullRequestMergeable { + MERGEABLE = 'MERGEABLE', + CONFLICTING = 'CONFLICTING', + UNKNOWN = 'UNKNOWN' +} + +export enum GithubPullRequestReviewState { + Pending, + Commented, + Approved, + ChangesRequested, + Dismissed +} + +export enum GithubPullRequestState { + open, + closed, + merged +} +export enum GithubReviewDecisionState { + ChangesRequested, + Approved, + ReviewRequired +} + +export interface LastReviewState { + user: Ref + state: GithubPullRequestReviewState +} +/** + * @public + */ +export interface GithubPullRequest extends Issue { + state: GithubPullRequestState + reviewDecision: GithubReviewDecisionState + + reviewers: Ref[] | null + + space: Ref + + head: GithubRepositoryRef + base: GithubRepositoryRef + + draft: boolean + + mergedAt: Timestamp | null + closedAt: Timestamp | null + + commits: number + + mergeable: PullRequestMergeable + + latestReviews: LastReviewState[] + + reviews: number + + reviewComments: number +} + +/** + * @public + */ +export interface GitRef { + url: string + sha: string +} + +/** + * @public + */ +export interface GithubPullRequestCommit extends AttachedDoc { + attachedTo: Ref + + sha: string + + author?: Ref + committer?: Ref + + message: string + + tree: GitRef + + verified: boolean + reason: string | null + signature: string | null + payload: string | null + + parents: GitRef[] +} + +/** + * @public + */ +export interface GithubReview extends ActivityMessage { + state: GithubPullRequestReviewState + body: Markup + + // Urls of comments + comments: string[] +} + +/** + * @public + */ +export interface GithubReviewViewlet extends ActivityMessageViewlet { + label?: IntlString +} + +export type MinimizeReason = 'abuse' | 'off-topic' | 'outdated' | 'resolved' | 'duplicate' | 'spam' + +/** + * @public + */ +export interface GithubPullRequestReview extends AttachedDoc { + attachedTo: Ref + + author: Ref + files: GithubPullRequestFileReview[] +} + +/** + * @public + */ +export interface GithubReviewThread extends ActivityMessage { + threadId: string + line: number + startLine: number + isOutdated: boolean + isResolved: boolean + diffSide: 'LEFT' | 'RIGHT' + isCollapsed: boolean + originalLine: number + originalStartLine: number | null + path: string + startDiffSide: 'LEFT' | 'RIGHT' | null + resolvedBy: Ref | null +} + +export interface GithubReviewComment extends AttachedDoc { + attachedTo: Ref // Attached to review thread. + url: string // Used to identify comment for review, etc. + reviewThreadId: string + reviewUrl: string // A review url. + + body: Markup + + outdated: boolean + + includesCreatedEdit: boolean + isMinimized: boolean + minimizedReason: MinimizeReason | null + + line: number | null + startLine: number | null + originalLine: number | null + originalStartLine: number | null + diffHunk: string | null + path: string + + replyToUrl?: string +} + +/** + * @public + */ +export interface GithubPatch extends Attachment {} + +/** + * @public + */ +export enum GithubIssueState { + Opened = 'open', + Closed = 'closed' +} +/** + * @public + */ +export enum GithubIssueStateReason { + Completed = 'COMPLETED', + Reopened = 'REOPENED', + NotPlanned = 'NOT_PLANNED' +} + +export interface IntegrationRepositoryData { + id: number + name: string + owner: GithubUserInfo + url: string + nodeId: string +} + +/** + * @public + */ +export interface GithubIntegrationRepository extends AttachedDoc { + name: string + repositoryId: number + deleted?: boolean + + // In case synchronization was configured. + githubProject?: Ref | null + + // Debug only + enabled: boolean + + id?: number + owner?: Data + url?: string + htmlURL?: string + nodeId?: string + + description?: string + fork: boolean + forks: number + private: boolean + stargazers: number + + hasIssues: boolean + hasProjects: boolean + hasDownloads: boolean + hasPages: boolean + hasWiki: boolean + hasDiscussions: boolean + + openIssues: number + watchers: number + + archived: boolean + + size: number + + language?: string + + visibility?: string + + updatedAt?: Timestamp + + resourcePath?: string +} + +/** + * @public + */ +export interface GithubIntegration extends Doc { + requestId?: string + // Unique identifier for the installation id, will be filled after installation redirect. + installationId: number + clientId: string + + // Technical details, will be filled by github integration service. + name: string // Organization or individual user name + nodeId: string + + // If alive, platform perform live synchronization of repository. + alive: boolean + + repositories: number + + // Organization or + type?: 'User' | 'Organization' | 'Bot' + byUser?: string +} +/** + * @public + */ +export interface GithubAuthentication extends Preference { + login: string + + avatar?: string + email?: string + bio?: string + blog?: string + company?: string + createdAt: Date + updatedAt: Date + followers: number + following: number + gravatarId?: string + location?: string + url: string + name?: string + nodeId: string + + repositories: number + openIssues: number + closedIssues: number + openPRs: number + mergedPRs: number + closedPRs: number + repositoryDiscussions: number + starredRepositories: number + + organizations: { + totalCount: number + nodes: { + url: string + avatarUrl: string | undefined + name: string | undefined + description: string | undefined + archivedAt: string | undefined + email: string | undefined + viewerIsAMember: boolean + updatedAt: string | undefined + descriptionHTML: string | undefined + location: string | undefined + websiteUrl: string | undefined + }[] + } + + error?: string | null + authRequestTime?: Timestamp +} + +/** + * @public + */ +export interface GithubUserInfo extends Doc { + login: string + id: string + email?: string + name?: string + avatarUrl?: string +} + +/** + * @public + */ +export interface GithubFieldMapping { + // Platform field + _id: Ref + name: string + _class: Ref> + + // Github information + githubId: string // It could be fieldName or fieldId +} + +/** + * @public + */ +export interface GithubProjectSyncData { + // Project NodeId + projectNodeId?: string + projectNumber?: number + + githubProjectName?: string + + // Mapping of all fields in this project. + mappings: GithubFieldMapping[] + + // Update mapping + githubUpdatedAt?: string +} + +/** + * @public + * + * Mixin to ordinary project, to allow github repository mapping into it. + */ +export interface GithubProject extends Project, GithubProjectSyncData { + integration: Ref + + // A list of mapped repositories we synchronized into this project. + repositories: Ref[] + + // Mixin to store all github custom attributes in + mixinClass: Ref> +} + +/** + * @public + * + * Mixin for milestone to represent a github project for it. + */ +export interface GithubMilestone extends Milestone, GithubProjectSyncData { + // A link to github project. + url: Hyperlink +} + +export interface GithubPullRequestReviewThread extends Doc { + githubId: string + line: number + startLine: number + isOutdated: boolean + isResolved: boolean + diffSide: 'LEFT' | 'RIGHT' + isCollapsed: boolean + originalLine: number + originalLineStart: number + path: string +} + +/** + * @public + */ + +export const githubPullRequestStates: TaskStatusFactory[] = [ + { category: task.statusCategory.Active, statuses: [['Review in progress', PaletteColorIndexes.Cerulean]] }, + { category: task.statusCategory.Won, statuses: [['Merged', PaletteColorIndexes.Grass]] }, + { category: task.statusCategory.Lost, statuses: [['Canceled', PaletteColorIndexes.Coin]] } +] + +export function makeQuery (obj: Record): string { + return Object.keys(obj) + .filter((it) => it[1] != null) + .map(function (k) { + return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k] as string | number | boolean) + }) + .join('&') +} + +/** + * @public + */ +export const githubId = 'github' as Plugin + +export default plugin(githubId, { + class: { + DocSyncInfo: '' as Ref>, + GithubIntegration: '' as Ref>, + GithubPullRequest: '' as Ref>, + GithubAuthentication: '' as Ref>, + GithubIntegrationRepository: '' as Ref>, + GithubPatch: '' as Ref>, + GithubUserInfo: '' as Ref>, + + GithubPullRequestReview: '' as Ref>, + GithubReview: '' as Ref>, + GithubReviewThread: '' as Ref>, + GithubReviewComment: '' as Ref> + }, + mixin: { + GithubIssue: '' as Ref>, + GithubProject: '' as Ref>, + GithubMilestone: '' as Ref>, + GithubComponent: '' as Ref>, + GithubUser: '' as Ref>, + GithubTodo: '' as Ref> + }, + icon: { + Github: '' as Asset, + GithubRepository: '' as Asset, + PullRequest: '' as Asset, + PullRequestMerged: '' as Asset, + PullRequestClosed: '' as Asset, + Forks: '' as Asset + }, + component: { + ConnectApp: '' as AnyComponent + }, + metadata: { + GithubApplication: '' as Metadata, + GithubClientID: '' as Metadata, + GithubURL: '' as Metadata + }, + descriptors: { + PullRequest: '' as Ref, + GithubProject: '' as Ref + }, + string: { + Issue: '' as IntlString, + PullRequest: '' as IntlString, + IssueConnectedActivityInfo: '' as IntlString, + PullRequestConnectedActivityInfo: '' as IntlString, + PullRequestMergedActivityInfo: '' as IntlString, + AuthenticatedWithGithub: '' as IntlString, + AuthenticationRevokedGithub: '' as IntlString, + AuthenticatedWithGithubEmployee: '' as IntlString, + AuthenticatedWithGithubRequired: '' as IntlString + } +}) diff --git a/services/github/github/tsconfig.json b/services/github/github/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/github/github/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/github/model-github/.eslintrc.js b/services/github/model-github/.eslintrc.js new file mode 100644 index 0000000000..c1cf82cba0 --- /dev/null +++ b/services/github/model-github/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/github/model-github/.npmignore b/services/github/model-github/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/github/model-github/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/github/model-github/config/rig.json b/services/github/model-github/config/rig.json new file mode 100644 index 0000000000..2f6be36605 --- /dev/null +++ b/services/github/model-github/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "model" +} diff --git a/services/github/model-github/package.json b/services/github/model-github/package.json new file mode 100644 index 0000000000..20517f3ad7 --- /dev/null +++ b/services/github/model-github/package.json @@ -0,0 +1,62 @@ +{ + "name": "@hcengineering/model-github", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Hardcore Engineering Inc.", + "template": "@hcengineering/model-package", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/contact": "^0.6.24", + "@hcengineering/chunter": "^0.6.20", + "@hcengineering/core": "^0.6.32", + "@hcengineering/model": "^0.6.11", + "@hcengineering/model-activity": "^0.6.0", + "@hcengineering/model-attachment": "^0.6.0", + "@hcengineering/model-chunter": "^0.6.0", + "@hcengineering/model-contact": "^0.6.1", + "@hcengineering/model-core": "^0.6.0", + "@hcengineering/model-notification": "^0.6.0", + "@hcengineering/model-preference": "^0.6.0", + "@hcengineering/model-presentation": "^0.6.0", + "@hcengineering/model-task": "^0.6.0", + "@hcengineering/model-tracker": "^0.6.0", + "@hcengineering/model-view": "^0.6.0", + "@hcengineering/model-workbench": "^0.6.1", + "@hcengineering/notification": "^0.6.23", + "@hcengineering/activity": "^0.6.0", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/preference": "^0.6.13", + "@hcengineering/setting": "^0.6.17", + "@hcengineering/tags": "^0.6.16", + "@hcengineering/task": "^0.6.20", + "@hcengineering/text": "^0.6.5", + "@hcengineering/tracker": "^0.6.24", + "@hcengineering/ui": "^0.6.15", + "@hcengineering/view": "^0.6.13", + "@hcengineering/github": "^0.6.0", + "@hcengineering/github-resources": "^0.6.0", + "@hcengineering/model-time": "^0.6.0", + "@hcengineering/time": "^0.6.0" + } +} diff --git a/services/github/model-github/src/index.ts b/services/github/model-github/src/index.ts new file mode 100644 index 0000000000..1f6832bb3b --- /dev/null +++ b/services/github/model-github/src/index.ts @@ -0,0 +1,1068 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +import { + ArrOf, + Collection, + Hidden, + Index, + Mixin, + Model, + Prop, + ReadOnly, + TypeAny, + TypeBoolean, + TypeDate, + TypeHyperlink, + TypeMarkup, + TypeNumber, + TypeRecord, + TypeRef, + TypeString, + UX, + type Builder +} from '@hcengineering/model' +import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core' +import github from './plugin' + +import { + DateRangeMode, + IndexKind, + type Account, + type Class, + type Data, + type Doc, + type Domain, + type Hyperlink, + type Markup, + type Ref, + type Timestamp +} from '@hcengineering/core' + +import { type Person } from '@hcengineering/contact' +import contact, { TContact } from '@hcengineering/model-contact' +import presentation from '@hcengineering/model-presentation' +import tracker, { TComponent, TIssue, TMilestone, TProject, issuesOptions } from '@hcengineering/model-tracker' +import view, { classPresenter } from '@hcengineering/model-view' +import workbench from '@hcengineering/model-workbench' +import { getEmbeddedLabel } from '@hcengineering/platform' +import setting from '@hcengineering/setting' +import tags from '@hcengineering/tags' +import task from '@hcengineering/task' +import { + type DocSyncInfo, + type GithubAuthentication, + type GithubComponent, + type GithubFieldMapping, + type GithubIntegration, + type GithubIntegrationRepository, + type GithubIssue, + type GithubMilestone, + type GithubPatch, + type GithubProject, + type GithubPullRequest, + type GithubPullRequestFileReview, + type GithubPullRequestReview, + type GithubPullRequestReviewState, + type GithubPullRequestState, + type GithubRepositoryRef, + type GithubReview, + type GithubReviewComment, + type GithubReviewDecisionState, + type GithubReviewThread, + type GithubTodo, + type GithubUser, + type GithubUserInfo, + type LastReviewState, + type MinimizeReason, + type PullRequestMergeable +} from '@hcengineering/github' + +import { generateClassNotificationTypes } from '@hcengineering/model-notification' + +import { type ActivityMessageControl } from '@hcengineering/activity' +import activity, { TActivityMessage } from '@hcengineering/model-activity' +import attachment, { TAttachment } from '@hcengineering/model-attachment' +import chunter from '@hcengineering/model-chunter' +import { TPreference } from '@hcengineering/model-preference' +import { TToDO } from '@hcengineering/model-time' +import notification from '@hcengineering/notification' +import { DOMAIN_PREFERENCE } from '@hcengineering/preference' +import time from '@hcengineering/time' + +export { githubId } from '@hcengineering/github' +export { githubOperation, githubOperationPreTime } from './migration' +export { default } from './plugin' +export const DOMAIN_GITHUB = 'github' as Domain +export const DOMAIN_GITHUB_COMMENTS = 'github_comments' as Domain + +@Model(github.class.DocSyncInfo, core.class.Doc, DOMAIN_GITHUB) +export class TDocSyncInfo extends TDoc implements DocSyncInfo { + // _id === objectId + @Prop(TypeNumber(), getEmbeddedLabel('Github number')) + @ReadOnly() + @Index(IndexKind.Indexed) + githubNumber!: number + + @Prop(TypeString(), getEmbeddedLabel('URL')) + @ReadOnly() + @Index(IndexKind.Indexed) + url!: string + + @Prop(TypeString(), getEmbeddedLabel('Parent URL')) + @ReadOnly() + @Index(IndexKind.Indexed) + parent!: string + + @Prop(TypeNumber(), getEmbeddedLabel('ID')) + @ReadOnly() + id!: number + + @Prop(TypeString(), getEmbeddedLabel('ObjectClass')) + @ReadOnly() + @Index(IndexKind.Indexed) + objectClass!: Ref> + + external!: Record + + @Prop(TypeString(), getEmbeddedLabel('Sync request')) + @ReadOnly() + @Index(IndexKind.Indexed) + needSync!: string + + @Prop(TypeString(), getEmbeddedLabel('External Sync request')) + @ReadOnly() + @Index(IndexKind.Indexed) + externalVersion!: string + + @Prop(TypeString(), getEmbeddedLabel('Derived request')) + @ReadOnly() + @Index(IndexKind.Indexed) + derivedVersion!: string + + @Prop(TypeRef(github.class.GithubIntegrationRepository), getEmbeddedLabel('Repository')) + @ReadOnly() + @Index(IndexKind.Indexed) + repository!: Ref | null + + @Prop(TypeString(), getEmbeddedLabel('Deleted flag')) + @ReadOnly() + @Index(IndexKind.Indexed) + deleted!: boolean +} + +@Model(github.class.GithubUserInfo, core.class.Doc, DOMAIN_GITHUB) +export class TGithubUserInfo extends TDoc implements GithubUserInfo { + @Prop(TypeString(), getEmbeddedLabel('ID')) + @ReadOnly() + id!: string + + @Prop(TypeString(), getEmbeddedLabel('login')) + @ReadOnly() + @Index(IndexKind.Indexed) + login!: string + + @Prop(TypeString(), getEmbeddedLabel('email')) + @ReadOnly() + @Index(IndexKind.Indexed) + email!: string + + @Prop(TypeString(), getEmbeddedLabel('name')) + @ReadOnly() + name!: string + + @Prop(TypeString(), getEmbeddedLabel('avatarUrl')) + @ReadOnly() + avatarUrl!: string +} + +@Model(github.class.GithubIntegration, core.class.Doc, DOMAIN_GITHUB) +@UX(github.string.Integrations) +export class TGithubIntegration extends TDoc implements GithubIntegration { + // Unique identifier for the installation id, will be filled after installation redirect. + installationId!: number + clientId!: string + + // Technical details, will be filled by github integration service. + @Prop(TypeString(), getEmbeddedLabel('Name')) + @ReadOnly() + name!: string // Organization or individual user name + + nodeId!: string + + @Prop(TypeBoolean(), getEmbeddedLabel('Alive')) + @ReadOnly() + alive!: boolean + + @Prop(Collection(github.class.GithubIntegrationRepository), getEmbeddedLabel('Repositories')) + @ReadOnly() + repositories!: number + + @Prop(TypeBoolean(), getEmbeddedLabel('User Project')) + useProject!: boolean +} + +@Model(github.class.GithubIntegrationRepository, core.class.AttachedDoc, DOMAIN_GITHUB) +export class TGithubIntegrationRepository extends TAttachedDoc implements GithubIntegrationRepository { + @Prop(TypeString(), getEmbeddedLabel('Name')) + @ReadOnly() + name!: string + + @Prop(TypeNumber(), getEmbeddedLabel('Repository ID')) + @ReadOnly() + repositoryId!: number + + @Prop(TypeBoolean(), getEmbeddedLabel('Alive')) + @ReadOnly() + enabled!: boolean + + @Prop(TypeRef(github.mixin.GithubProject), getEmbeddedLabel('Repository')) + @ReadOnly() + githubProject!: Ref | null + + @Prop(TypeNumber(), getEmbeddedLabel('ID')) + @ReadOnly() + id?: number + + @Prop(TypeRecord(), getEmbeddedLabel('Owner')) + @ReadOnly() + owner?: Data + + @Prop(TypeString(), getEmbeddedLabel('URL')) + @ReadOnly() + url?: string + + @Prop(TypeString(), getEmbeddedLabel('HTMLURL')) + @ReadOnly() + htmlURL?: string + + @Prop(TypeString(), getEmbeddedLabel('NodeID')) + @ReadOnly() + nodeId?: string + + @Prop(TypeString(), getEmbeddedLabel('Description')) + @ReadOnly() + description?: string + + @Prop(TypeBoolean(), getEmbeddedLabel('Is Fork')) + @ReadOnly() + fork!: boolean + + @Prop(TypeNumber(), getEmbeddedLabel('Forks')) + @ReadOnly() + forks!: number + + @Prop(TypeBoolean(), getEmbeddedLabel('Private')) + @ReadOnly() + private!: boolean + + @Prop(TypeNumber(), getEmbeddedLabel('Stars')) + @ReadOnly() + stargazers!: number + + @Prop(TypeBoolean(), getEmbeddedLabel('Has Issues')) + @ReadOnly() + hasIssues!: boolean + + @Prop(TypeBoolean(), getEmbeddedLabel('Has Projects')) + @ReadOnly() + hasProjects!: boolean + + @Prop(TypeBoolean(), getEmbeddedLabel('Has Downloads')) + @ReadOnly() + hasDownloads!: boolean + + @Prop(TypeBoolean(), getEmbeddedLabel('Has Pages')) + @ReadOnly() + hasPages!: boolean + + @Prop(TypeBoolean(), getEmbeddedLabel('Has Wiki')) + @ReadOnly() + hasWiki!: boolean + + @Prop(TypeBoolean(), getEmbeddedLabel('Has Discussions')) + @ReadOnly() + hasDiscussions!: boolean + + @Prop(TypeNumber(), getEmbeddedLabel('Open Issues')) + @ReadOnly() + openIssues!: number + + @Prop(TypeNumber(), getEmbeddedLabel('Watchers')) + @ReadOnly() + watchers!: number + + @Prop(TypeBoolean(), getEmbeddedLabel('Archived')) + @ReadOnly() + archived!: boolean + + @Prop(TypeNumber(), getEmbeddedLabel('Size')) + @ReadOnly() + size!: number + + @Prop(TypeString(), getEmbeddedLabel('Language')) + @ReadOnly() + language?: string + + @Prop(TypeString(), getEmbeddedLabel('Visibility')) + @ReadOnly() + visibility?: string +} + +@Model(github.class.GithubAuthentication, core.class.Doc, DOMAIN_PREFERENCE) +export class TGithubAuthentication extends TPreference implements GithubAuthentication { + code!: string + token!: string + expiresIn!: number // seconds + refreshToken!: string + refreshTokenExpiresIn!: number + login!: string + authorized!: boolean + state!: string + scope!: string + error!: string | null + + avatar!: string + email!: string + bio!: string + blog!: string + company!: string + createdAt!: Date + updatedAt!: Date + followers!: number + following!: number + gravatarId!: string + location!: string + url!: string + name!: string + nodeId!: string + + repositories!: number + openIssues!: number + closedIssues!: number + openPRs!: number + mergedPRs!: number + closedPRs!: number + repositoryDiscussions!: number + starredRepositories!: number + + organizations!: GithubAuthentication['organizations'] +} + +@Mixin(github.mixin.GithubProject, tracker.class.Project) +@UX(github.string.Repository) +export class TGithubProject extends TProject implements GithubProject { + @Prop(TypeRef(github.class.GithubIntegration), getEmbeddedLabel('Integration')) + @ReadOnly() + integration!: Ref + + @Prop(ArrOf(TypeRef(github.class.GithubIntegrationRepository)), getEmbeddedLabel('Repositories')) + @ReadOnly() + repositories!: Ref[] + + @Prop(TypeString(), getEmbeddedLabel('NodeID')) + @ReadOnly() + projectNodeId!: string + + @Prop(TypeNumber(), getEmbeddedLabel('Number')) + @ReadOnly() + projectNumber!: number + + @Prop(TypeRef(core.class.Class), getEmbeddedLabel('Attribute Class')) + @ReadOnly() + mixinClass!: Ref> + + @Prop(ArrOf(TypeRecord()), getEmbeddedLabel('Field mappings')) + // Mapping of all fields in this project. + mappings!: GithubFieldMapping[] +} + +@Mixin(github.mixin.GithubIssue, tracker.class.Issue) +@UX(github.string.GithubIssue, github.icon.Github) +export class TGithubIssue extends TIssue implements GithubIssue { + @Prop(TypeHyperlink(), getEmbeddedLabel('Github URL')) + @Index(IndexKind.FullText) + @ReadOnly() + @Hidden() + url!: Hyperlink + + @Prop(TypeNumber(), tracker.string.Number) + @Index(IndexKind.FullText) + @ReadOnly() + @Hidden() + githubNumber!: number + + @Prop(TypeRef(github.class.GithubIntegrationRepository), getEmbeddedLabel('Repository')) + repository!: Ref +} + +@Mixin(github.mixin.GithubTodo, time.class.ToDo) +export class TGithubTodo extends TToDO implements GithubTodo { + purpose!: 'review' | 'fix' +} + +@Mixin(github.mixin.GithubUser, contact.class.Contact) +@UX(github.string.GithubUser, github.icon.Github) +export class TGithubUser extends TContact implements GithubUser { + @Prop(TypeHyperlink(), getEmbeddedLabel('Github URL')) + @Index(IndexKind.FullText) + @ReadOnly() + url!: Hyperlink +} + +@Mixin(github.mixin.GithubComponent, tracker.class.Component) +@UX(github.string.Repository, github.icon.Github) +export class TGithubComponent extends TComponent implements GithubComponent { + @Prop(TypeRef(github.class.GithubIntegrationRepository), getEmbeddedLabel('Repository')) + @ReadOnly() + @Hidden() + repository!: Ref + + @Prop(TypeBoolean(), getEmbeddedLabel('Represent')) + @ReadOnly() + @Hidden() + represent!: boolean +} + +@Mixin(github.mixin.GithubMilestone, tracker.class.Milestone) +@UX(github.string.GithubIssue) +export class TGithubMilestone extends TMilestone implements GithubMilestone { + @Prop(TypeHyperlink(), getEmbeddedLabel('Github URL')) + @Index(IndexKind.FullText) + @ReadOnly() + url!: Hyperlink + + @Prop(TypeString(), getEmbeddedLabel('NodeID')) + @ReadOnly() + projectNodeId!: string + + @Prop(TypeNumber(), getEmbeddedLabel('Number')) + @ReadOnly() + projectNumber!: number + + @Prop(ArrOf(TypeRecord()), getEmbeddedLabel('Field mappings')) + // Mapping of all fields in this project. + mappings!: GithubFieldMapping[] +} + +@Model(github.class.GithubPullRequest, tracker.class.Issue) +@UX(github.string.PullRequest, github.icon.PullRequest, undefined, undefined, undefined, github.string.PullRequests) +export class TGithubPullRequest extends TIssue implements GithubPullRequest { + @Prop(ArrOf(TypeRef(contact.class.Person)), getEmbeddedLabel('Reviewers')) + reviewers!: Ref[] | null + + @Prop(TypeRef(tracker.class.Project), tracker.string.Project, { icon: tracker.icon.Issues }) + @Index(IndexKind.Indexed) + @ReadOnly() + declare space: Ref + + head!: GithubRepositoryRef + base!: GithubRepositoryRef + + @Prop(TypeBoolean(), github.string.PRDraft) + @ReadOnly() + draft!: boolean + + @Prop(TypeDate(DateRangeMode.DATETIME), github.string.PRMergedAt) + @ReadOnly() + mergedAt!: Timestamp | null + + @Prop(TypeDate(DateRangeMode.DATETIME), github.string.PRClosedAt) + @ReadOnly() + closedAt!: Timestamp | null + + @Prop(TypeNumber(), github.string.PRFile) + @ReadOnly() + @Hidden() + files!: number + + @Prop(TypeNumber(), github.string.PRCommit) + @ReadOnly() + commits!: number + + @Prop(TypeAny(github.component.MergeableValuePresenter, github.string.Mergeable), github.string.Mergeable) + @ReadOnly() + mergeable!: PullRequestMergeable + + @Prop( + TypeAny(github.component.PullRequestStateValuePresenter, github.string.PullRequestMergeState), + github.string.PullRequestMergeState + ) + @ReadOnly() + state!: GithubPullRequestState + + @Prop( + TypeAny(github.component.PullRequestReviewDecisionValuePresenter, github.string.PullRequestReviewDecision), + github.string.PullRequestReviewDecision + ) + @ReadOnly() + @Hidden() + reviewDecision!: GithubReviewDecisionState + + latestReviews!: LastReviewState[] + + @Prop(Collection(github.class.GithubPullRequestReview), github.string.PRReview) + @Hidden() + reviews!: number + + @Prop(Collection(github.class.GithubReviewComment), getEmbeddedLabel('Review Comments')) + @Hidden() + reviewComments!: number +} + +@Model(github.class.GithubPullRequestReview, core.class.AttachedDoc, DOMAIN_GITHUB) +export class TGithubPullRequestReview extends TAttachedDoc implements GithubPullRequestReview { + declare attachedTo: Ref + + author!: Ref + files!: GithubPullRequestFileReview[] +} + +@Model(github.class.GithubReview, activity.class.ActivityMessage) +export class TGithubReview extends TActivityMessage implements GithubReview { + @Prop(TypeString(), getEmbeddedLabel('State')) + state!: GithubPullRequestReviewState + + @Prop(TypeMarkup(), getEmbeddedLabel('Body')) + body!: Markup + + @Prop(ArrOf(TypeString()), getEmbeddedLabel('Comment refs')) + // Urls of comments + comments!: string[] +} + +@Model(github.class.GithubReviewThread, activity.class.ActivityMessage) +export class TGithubReviewThread extends TActivityMessage implements GithubReviewThread { + line!: number + startLine!: number + isOutdated!: boolean + isResolved!: boolean + diffSide!: 'LEFT' | 'RIGHT' + isCollapsed!: boolean + originalLine!: number + originalStartLine!: number | null + path!: string + startDiffSide!: 'LEFT' | 'RIGHT' | null + resolvedBy!: Ref | null + threadId!: string +} + +@Model(github.class.GithubReviewComment, core.class.AttachedDoc, DOMAIN_GITHUB_COMMENTS) +@UX(getEmbeddedLabel('Review Comment')) +export class TGithubReviewComment extends TAttachedDoc implements GithubReviewComment { + reviewThreadId!: string + declare attachedTo: Ref // Attached to review thread. + url!: string // Used to identify comment for review, etc. + reviewUrl!: string // A review url. + + @Prop(TypeMarkup(), getEmbeddedLabel('Body')) + body!: Markup + + outdated!: boolean + + includesCreatedEdit!: boolean + isMinimized!: boolean + minimizedReason!: MinimizeReason + + line!: number | null + startLine!: number | null + originalLine!: number | null + originalStartLine!: number | null + diffHunk!: string | null + path!: string + + replyToUrl!: string +} + +@Model(github.class.GithubPatch, attachment.class.Attachment) +export class TGithubPatch extends TAttachment implements GithubPatch {} + +export function createModel (builder: Builder): void { + builder.createModel( + TDocSyncInfo, + TGithubIssue, + TGithubIntegration, + TGithubProject, + TGithubPullRequest, + TGithubPullRequestReview, + TGithubReview, + TGithubReviewThread, + TGithubReviewComment, + TGithubAuthentication, + TGithubIntegrationRepository, + TGithubPatch, + TGithubUserInfo, + TGithubMilestone, + TGithubComponent, + TGithubUser, + TGithubTodo + ) + + builder.createDoc( + setting.class.IntegrationType, + core.space.Model, + { + label: github.string.Github, + description: github.string.GithubDesc, + icon: github.component.GithubIcon, + allowMultiple: false, + createComponent: github.component.Connect, + configureComponent: github.component.Configure + }, + github.integrationType.Github + ) + + builder.mixin(github.class.GithubPullRequest, core.class.Class, view.mixin.ObjectPresenter, { + presenter: github.component.PullRequestPresenter + }) + + builder.mixin(github.class.GithubPullRequest, core.class.Class, activity.mixin.ActivityDoc, {}) + + classPresenter( + builder, + github.class.GithubIntegrationRepository, + github.component.RepositoryPresenterRef, + github.component.RepositoryPresenterRefEditor + ) + + builder.mixin(github.mixin.GithubIssue, core.class.Class, view.mixin.ObjectPresenter, { + presenter: github.component.GithubIssuePresenter + }) + + builder.mixin(github.class.GithubIntegrationRepository, core.class.Class, view.mixin.ObjectPresenter, { + presenter: github.component.RepositoryPresenter + }) + + builder.mixin(github.class.GithubPullRequest, core.class.Class, notification.mixin.NotificationObjectPresenter, { + presenter: github.component.PullRequestNotificationPresenter + }) + + builder.mixin(github.class.GithubPullRequest, core.class.Class, view.mixin.ObjectEditorFooter, { + editor: github.component.EditPullRequest + }) + + builder.mixin(github.class.GithubPullRequest, core.class.Class, notification.mixin.ClassCollaborators, { + fields: ['createdBy', 'assignee', 'reviewers'] + }) + + builder.createDoc(presentation.class.DocCreateExtension, core.space.Model, { + ofClass: tracker.class.Issue, + apply: github.functions.UpdateIssue, + components: { + createButton: github.component.GithubIssueInfoHeader + } + }) + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: github.class.GithubPullRequest, // Since it extends class, configuration will be merged with Issues;s one. + descriptor: view.viewlet.List, + viewOptions: issuesOptions(false), + configOptions: { + strict: true, + hiddenKeys: [ + 'title', + 'blockedBy', + 'relations', + 'description', + 'number', + 'reportedTime', + 'reports', + 'priority', + 'component', + 'milestone', + 'estimation', + 'remainingTime', + 'status', + 'dueDate', + 'attachedTo', + 'createdBy', + 'modifiedBy' + ] + }, + config: [ + { + key: '', + label: tracker.string.Priority, + presenter: tracker.component.PriorityEditor, + props: { type: 'priority', kind: 'list', size: 'small' }, + displayProps: { key: 'priority' } + }, + { + key: '', + label: tracker.string.Identifier, + presenter: tracker.component.IssuePresenter, + displayProps: { key: 'issue', fixed: 'left' } + }, + { + key: '', + label: tracker.string.Status, + presenter: tracker.component.StatusEditor, + props: { kind: 'list', size: 'small', justify: 'center' }, + displayProps: { key: 'status' } + }, + { + key: '', + label: github.string.Mergeable, + presenter: github.component.PullRequestMergeState, + props: { small: true }, + displayProps: { key: 'merge_state' } + }, + { + key: '', + label: tracker.string.Title, + presenter: github.component.TitlePresenter, + props: { shouldUseMargin: true, showParent: false }, + displayProps: { key: 'title' } + }, + { + key: '', + label: tracker.string.SubIssues, + presenter: tracker.component.SubIssuesSelector, + props: {} + }, + { key: 'comments', displayProps: { key: 'comments', suffix: true } }, + { key: 'attachments', displayProps: { key: 'attachments', suffix: true } }, + { key: '', displayProps: { grow: true } }, + { + key: 'labels', + presenter: tags.component.LabelsPresenter, + displayProps: { compression: true }, + props: { kind: 'list', full: false } + }, + { + key: '', + label: getEmbeddedLabel('*'), + presenter: tracker.component.IssueExtra, + displayProps: { compression: true }, + props: { kind: 'list', full: false } + }, + { + key: '', + label: tracker.string.Milestone, + presenter: tracker.component.MilestoneEditor, + props: { + kind: 'list', + size: 'small', + shouldShowPlaceholder: false + }, + displayProps: { + key: 'milestone', + excludeByKey: 'milestone', + compression: true + } + }, + { + key: '', + label: tracker.string.Component, + presenter: tracker.component.ComponentEditor, + props: { + kind: 'list', + size: 'small', + shouldShowPlaceholder: false + }, + displayProps: { + key: 'component', + excludeByKey: 'component', + compression: true + } + }, + { + key: '', + label: tracker.string.DueDate, + presenter: tracker.component.DueDatePresenter, + displayProps: { key: 'dueDate', compression: true }, + props: { kind: 'list' } + }, + { + key: '', + label: tracker.string.Estimation, + presenter: tracker.component.EstimationEditor, + props: { kind: 'list', size: 'small' }, + displayProps: { key: 'estimation', fixed: 'left', dividerBefore: true, optional: true } + }, + { + key: 'modifiedOn', + presenter: tracker.component.ModificationDatePresenter, + displayProps: { key: 'submodified', fixed: 'left', dividerBefore: true } + }, + { + key: '', + presenter: tracker.component.AssigneeEditor, + displayProps: { key: 'assignee', fixed: 'right' }, + props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' } + } + ], + options: { + lookup: { + space: tracker.class.Project + } + } + }, + github.viewlet.PullRequests + ) + + builder.createDoc(workbench.class.ApplicationNavModel, core.space.Model, { + extends: tracker.app.Tracker, + spaces: [ + { + id: 'projects', + spaceClass: tracker.class.Project, + specials: [ + { + id: 'pulls', + label: github.string.PullRequests, + icon: tracker.icon.Issue, + visibleIf: github.functions.ShowForRepositoryOnly, + component: github.component.PullRequests, + componentProps: { + title: github.string.PullRequests, + config: [ + ['all', github.string.All, {}], + ['active', github.string.Active, {}], + ['closed', github.string.Closed, {}] + ] + } + } + ] + } + ] + }) + + builder.createDoc( + notification.class.NotificationGroup, + core.space.Model, + { + label: github.string.Github, + icon: github.icon.Github, + objectClass: github.class.GithubPullRequest + }, + github.ids.GithubNotificationGroup + ) + + builder.createDoc( + notification.class.NotificationType, + core.space.Model, + { + hidden: false, + generated: false, + label: task.string.AssignedToMe, + group: github.ids.GithubNotificationGroup, + field: 'assignee', + txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc], + objectClass: github.class.GithubPullRequest, + templates: { + textTemplate: 'Pull request {doc} was assigned to you by {sender}', + htmlTemplate: '

Pull request {doc} was assigned to you by {sender}

', + subjectTemplate: 'Pull request {doc} was assigned to you' + }, + defaultEnabled: true + }, + github.ids.AssigneeNotification + ) + + generateClassNotificationTypes( + builder, + github.class.GithubPullRequest, + github.ids.GithubNotificationGroup, + [], + ['comments', 'status', 'assignee', 'milestone'] + ) + + builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, { + extension: tracker.extensions.IssueListHeader, + component: github.component.AuthenticationCheck + }) + + builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, { + extension: tracker.extensions.EditIssueHeader, + component: github.component.AuthenticationCheck, + props: { + kind: 'ghost' + } + }) + + builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, { + extension: tracker.extensions.EditIssueTitle, + component: github.component.PullRequestMergeState, + props: { + kind: 'ghost' + } + }) + + builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, { + extension: tracker.extensions.EditIssueTitle, + component: github.component.GithubIssueHeader, + props: { + kind: 'ghost' + } + }) + + builder.mixin(github.class.GithubPullRequest, core.class.Class, view.mixin.IgnoreActions, { + actions: [view.action.Delete, task.action.Move, tracker.action.MoveToProject] + }) + + builder.createDoc(presentation.class.DocRules, core.space.Model, { + ofClass: tracker.class.Issue, + fieldRules: [ + { + field: 'assignee', + query: {}, + mixin: github.mixin.GithubIssue, + fieldQuery: { + [github.mixin.GithubUser + '.url']: { $exists: true } + }, + fieldQueryFill: {}, + allowConflict: false, + disableUnset: true, + disableEdit: true + } + ] + }) + + builder.mixin(github.class.DocSyncInfo, core.class.Class, core.mixin.IndexConfiguration, { + indexes: [], + searchDisabled: true + }) + + builder.createDoc( + chunter.class.ChatMessageViewlet, + core.space.Model, + { + messageClass: chunter.class.ChatMessage, + objectClass: github.class.GithubPullRequest, + label: chunter.string.LeftComment + }, + github.ids.GitHubPullRequestChatMessageViewlet + ) + + builder.createDoc(activity.class.ActivityExtension, core.space.Model, { + ofClass: github.class.GithubPullRequest, + components: { + input: chunter.component.ChatMessageInput + } + }) + + builder.createDoc( + task.class.ProjectTypeDescriptor, + core.space.Model, + { + baseClass: github.mixin.GithubProject, + description: github.string.GithubDesc, + icon: github.icon.Github, + name: github.string.Github, + availablePermissions: [core.permission.ForbidDeleteObject] + }, + github.descriptors.GithubProject + ) + + builder.createDoc( + task.class.TaskTypeDescriptor, + core.space.Model, + { + baseClass: github.class.GithubPullRequest, + allowCreate: false, + description: github.string.PullRequest, + icon: github.icon.PullRequest, + name: github.string.Github + }, + github.descriptors.PullRequest + ) + + builder.createDoc( + presentation.class.ObjectSearchCategory, + core.space.Model, + { + title: github.string.PullRequests, + icon: github.icon.PullRequest, + label: github.string.PullRequests, + query: tracker.completion.IssueQuery, + context: ['search', 'mention', 'spotlight'], + classToSearch: github.class.GithubPullRequest + }, + github.completion.PullRequestCategory + ) + + // Activity filters + + // We should skip activity github mixin stuff. + builder.createDoc(activity.class.ActivityMessageControl, core.space.Model, { + objectClass: tracker.class.Issue, + skip: [{ _class: core.class.TxMixin, mixin: github.mixin.GithubIssue }] + }) + + builder.createDoc(activity.class.ActivityMessageControl, core.space.Model, { + objectClass: contact.class.Person, + skip: [{ _class: core.class.TxMixin, mixin: github.mixin.GithubUser }] + }) + + builder.mixin(github.class.GithubReviewComment, core.class.Class, activity.mixin.IgnoreActivity, {}) + builder.mixin(github.class.GithubPullRequestReview, core.class.Class, activity.mixin.IgnoreActivity, {}) + builder.mixin(github.class.GithubReview, core.class.Class, view.mixin.ObjectPresenter, { + presenter: github.component.GithubReviewPresenter + }) + + builder.mixin(github.class.GithubReviewThread, core.class.Class, view.mixin.ObjectPresenter, { + presenter: github.component.GithubReviewThreadPresenter + }) + + builder.createDoc>( + activity.class.ActivityMessageControl, + core.space.Model, + { + objectClass: github.class.GithubPullRequest, + skip: [], + skipFields: [ + 'head', + 'base', + 'mergedAt', + 'closedAt', + 'commits', + 'mergeable', + 'reviews', + 'reviewComments', + 'latestReviews', + 'reviewDecision', + 'state' + ] + } + ) + + builder.createDoc(activity.class.DocUpdateMessageViewlet, core.space.Model, { + objectClass: github.class.GithubPullRequest, + action: 'update', + icon: github.icon.PullRequest, + config: { + status: { + iconPresenter: tracker.component.IssueStatusIcon + }, + priority: { + iconPresenter: tracker.component.PriorityIconPresenter + }, + estimation: { + icon: tracker.icon.Estimation + } + } + }) + + builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, { + domain: DOMAIN_GITHUB, + disabled: [ + { modifiedOn: 1 }, + { modifiedBy: 1 }, + { createdBy: 1 }, + { attachedToClass: 1 }, + { createdOn: -1 }, + { repository: 1 }, + { deleted: 1 }, + { githubNumber: 1 } + ] + }) + builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, { + domain: DOMAIN_GITHUB_COMMENTS, + disabled: [{ modifiedOn: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { attachedToClass: 1 }, { createdOn: -1 }] + }) +} diff --git a/services/github/model-github/src/migration.ts b/services/github/model-github/src/migration.ts new file mode 100644 index 0000000000..af5e4e6188 --- /dev/null +++ b/services/github/model-github/src/migration.ts @@ -0,0 +1,336 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +import core, { toIdMap, type AnyAttribute, type Ref, type Status } from '@hcengineering/core' +import { + tryMigrate, + tryUpgrade, + type MigrateOperation, + type MigrateUpdate, + type MigrationClient, + type MigrationDocumentQuery, + type MigrationIterator, + type MigrationUpgradeClient +} from '@hcengineering/model' +import chunter from '@hcengineering/model-chunter' +import { DOMAIN_SPACE } from '@hcengineering/model-core' +import { DOMAIN_TASK } from '@hcengineering/model-task' +import task from '@hcengineering/task' +import { htmlToMarkup } from '@hcengineering/text' +import tracker, { type Component, type Issue, type Project } from '@hcengineering/tracker' +import { + GithubPullRequestState, + githubId, + type DocSyncInfo, + type GithubIntegration, + type GithubIntegrationRepository, + type GithubPullRequest +} from '@hcengineering/github' +import github from './plugin' + +import { DOMAIN_TIME } from '@hcengineering/model-time' +import { DOMAIN_TRACKER } from '@hcengineering/model-tracker' +import time from '@hcengineering/time' +import { DOMAIN_GITHUB } from '.' + +export async function guessStatus (status: Status, statuses: Status[]): Promise { + const active = (): Status => statuses.find((it) => it.category === task.statusCategory.Active) as Status + + const lost = (): Status => statuses.find((it) => it.category === task.statusCategory.Lost) as Status + const won = (): Status => statuses.find((it) => it.category === task.statusCategory.Won) as Status + + if (status.category === task.statusCategory.Won) { + return won() + } + if (status.category === task.statusCategory.Lost) { + return lost() + } + return active() +} +async function migratePullRequests (client: MigrationClient): Promise { + await client.update( + DOMAIN_TASK, + { _class: github.class.GithubPullRequest, patch: { $exists: true } }, + { $unset: { patch: 1 } } + ) + await client.update( + DOMAIN_GITHUB, + { _class: github.class.GithubPullRequest, 'external.patch': { $ne: true } }, + { $set: { patch: false } } + ) + + const integrations = await client.find(DOMAIN_GITHUB, { _class: github.class.GithubIntegration }) + for (const i of integrations) { + if (typeof i.installationId === 'string') { + // We need to resync all integration + await client.update(DOMAIN_GITHUB, { _id: i._id }, { installationId: parseInt(i.installationId) }) + } + } + await client.update( + DOMAIN_GITHUB, + { _class: github.class.DocSyncInfo, lastModified: { $exists: false } }, + { $set: { needUpdate: true } } + ) +} + +async function migrateMissingStates (client: MigrationClient): Promise { + const prTaskTypes = toIdMap( + client.model.findAllSync(task.class.TaskType, { descriptor: github.descriptors.PullRequest }) + ) + + const allActiveStatuses = client.model.findAllSync(core.class.Status, { category: task.statusCategory.Active }) + + const wonStatuses = client.model.findAllSync(core.class.Status, { category: task.statusCategory.Won }) + + // We need to migrate Pull requests with merged to have a proper merged status, and not closed. + const merged = await client.find(DOMAIN_TASK, { + _class: github.class.GithubPullRequest, + state: GithubPullRequestState.merged + }) + for (const m of merged) { + const tt = prTaskTypes.get(m.kind) + if (tt === undefined) { + return + } + const merged = wonStatuses.find((it) => tt.statuses.includes(it._id)) + + if (merged !== undefined && m.status !== merged._id) { + await client.update(DOMAIN_TASK, { _id: m._id }, { status: merged._id }) + } + } + + const activePRs = await client.find(DOMAIN_TASK, { + _class: github.class.GithubPullRequest, + state: GithubPullRequestState.open + }) + for (const m of activePRs) { + const tt = prTaskTypes.get(m.kind) + if (tt === undefined) { + return + } + const active = allActiveStatuses.find((it) => tt.statuses.includes(it._id)) + + if (active !== undefined && m.status !== active._id) { + await client.update(DOMAIN_TASK, { _id: m._id }, { status: active._id }) + } + } + // +} + +async function migrateDocSyncInfo (client: MigrationClient): Promise { + await client.update( + DOMAIN_GITHUB, + { _class: github.class.DocSyncInfo, objectClass: 'chunter:class:Comment' }, + { objectClass: chunter.class.ChatMessage } + ) +} + +export const githubOperation: MigrateOperation = { + async migrate (client: MigrationClient): Promise { + await tryMigrate(client, githubId, [ + { + state: 'pull-requests', + func: migratePullRequests + }, + { + state: 'update-doc-sync-info', + func: migrateDocSyncInfo + } + ]) + }, + async upgrade (state: Map>, client: () => Promise): Promise { + await tryUpgrade(state, client, githubId, []) + } +} + +async function migrateTodoSpaces (client: MigrationClient): Promise { + await client.update( + DOMAIN_TIME, + { _class: time.class.ToDo, [github.mixin.GithubTodo]: { $exists: true } }, + { $set: { space: time.space.ToDos } } + ) +} + +async function migrateFixMissingDocSyncInfo (client: MigrationClient): Promise { + const projects = await client.find(DOMAIN_SPACE, { + _class: tracker.class.Project, + [github.mixin.GithubProject]: { $exists: true } + }) + for (const p of projects) { + const issues = await client.traverse( + DOMAIN_TASK, + { + _class: tracker.class.Issue, + space: p._id as Ref + }, + { + projection: { + _class: 1, + space: 1, + _id: 1, + attachedTo: 1, + modifiedBy: 1, + modifiedOn: 1 + } + } + ) + let counter = 0 + while (true) { + const docs = await issues.next(1000) + if (docs === null || docs.length === 0) { + break + } + const infos = await client.find( + DOMAIN_GITHUB, + { + _class: github.class.DocSyncInfo, + _id: { $in: docs.map((it) => it._id as unknown as Ref) } + }, + { + projection: { + _id: 1 + } + } + ) + const infoIds = toIdMap(infos) + let repository: Ref | null = null + for (const issue of docs) { + if (!infoIds.has(issue._id)) { + if (client.hierarchy.hasMixin(issue, github.mixin.GithubIssue)) { + repository = client.hierarchy.as(issue, github.mixin.GithubIssue).repository + } + counter++ + // Missing + await client.create(DOMAIN_GITHUB, { + _class: github.class.DocSyncInfo, + _id: issue._id as any, + url: '', + githubNumber: 0, + repository, + objectClass: issue._class, + externalVersion: '#', // We need to put this one to handle new documents. + needSync: '', + derivedVersion: '', + attachedTo: issue.attachedTo ?? tracker.ids.NoParent, + space: issue.space, + modifiedBy: issue.modifiedBy, + modifiedOn: issue.modifiedOn + }) + } + } + } + if (counter > 0) { + console.log('Created', counter, 'DocSyncInfos') + } + } +} + +async function migrateRemoveGithubComponents (client: MigrationClient): Promise { + const githubComponents = await client.find(DOMAIN_TRACKER, { + _class: tracker.class.Component, + [github.mixin.GithubComponent]: { $exists: true } + }) + await client.update( + DOMAIN_TASK, + { + _class: { $in: [tracker.class.Issue, github.class.GithubPullRequest] }, + component: { $in: Array.from(githubComponents.map((it) => it._id)) } + }, + { component: null } + ) + await client.deleteMany(DOMAIN_TRACKER, { _id: { $in: Array.from(githubComponents.map((it) => it._id)) } }) +} + +async function migrateMarkup (client: MigrationClient): Promise { + const hierarchy = client.hierarchy + const classes = hierarchy.getDescendants(core.class.Doc) + for (const _class of classes) { + const attributes = hierarchy.getAllAttributes(_class) + const filtered = Array.from(attributes.values()).filter((attribute) => { + return ( + hierarchy.isDerived(attribute.type._class, core.class.TypeMarkup) || + hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeMarkup) + ) + }) + if (filtered.length === 0) continue + + const iterator = await client.traverse(DOMAIN_GITHUB, { + _class: github.class.DocSyncInfo, + objectClass: _class, + current: { $exists: true } + }) + + try { + await processMigrateMarkupFor(filtered, client, iterator) + } finally { + await iterator.close() + } + } +} + +async function processMigrateMarkupFor ( + attributes: AnyAttribute[], + client: MigrationClient, + iterator: MigrationIterator +): Promise { + let processed = 0 + while (true) { + const docs = await iterator.next(1000) + if (docs === null || docs.length === 0) { + break + } + + const operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] + + for (const doc of docs) { + const update: MigrateUpdate = {} + + for (const attribute of attributes) { + const value = doc.current[attribute.name] + if (value != null) { + update[`current.${attribute.name}`] = htmlToMarkup(value) + } + } + + if (Object.keys(update).length > 0) { + operations.push({ filter: { _id: doc._id }, update }) + } + } + + if (operations.length > 0) { + await client.bulk(DOMAIN_GITHUB, operations) + } + + processed += docs.length + console.log('...processed', processed) + } +} + +export const githubOperationPreTime: MigrateOperation = { + async migrate (client: MigrationClient): Promise { + await tryMigrate(client, githubId, [ + { + state: 'fix-todo-spaces', + func: migrateTodoSpaces + }, + { + state: 'fix-missing-doc-sync-info', + func: migrateFixMissingDocSyncInfo + }, + { + state: 'remove-github-components', + func: migrateRemoveGithubComponents + }, + { + state: 'markup', + func: migrateMarkup + }, + { + state: 'migrate-missing-states', + func: migrateMissingStates + } + ]) + }, + async upgrade (state: Map>, client: () => Promise): Promise {} +} diff --git a/services/github/model-github/src/plugin.ts b/services/github/model-github/src/plugin.ts new file mode 100644 index 0000000000..44cf12ff12 --- /dev/null +++ b/services/github/model-github/src/plugin.ts @@ -0,0 +1,68 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform' +import { githubId } from '@hcengineering/github' +import github from '@hcengineering/github-resources/src/plugin' + +import { type ChatMessageViewlet } from '@hcengineering/chunter' +import { type Doc, type Ref, type Space } from '@hcengineering/core' +import { type DocCreateFunction, type ObjectSearchCategory } from '@hcengineering/model-presentation' +import { type NotificationGroup } from '@hcengineering/notification' +import type { AnyComponent } from '@hcengineering/ui' +import { type ActionCategory, type Viewlet } from '@hcengineering/view' + +export default mergeIds(githubId, github, { + component: { + Connect: '' as AnyComponent, + Configure: '' as AnyComponent, + PullRequestPresenter: '' as AnyComponent, + TitlePresenter: '' as AnyComponent, + PullRequestNotificationPresenter: '' as AnyComponent, + AuthenticationCheck: '' as AnyComponent, + GithubIssueHeader: '' as AnyComponent, + GithubReviewPresenter: '' as AnyComponent, + GithubReviewThreadPresenter: '' as AnyComponent, + + MergeableValuePresenter: '' as AnyComponent, + PullRequestStateValuePresenter: '' as AnyComponent, + PullRequestReviewDecisionValuePresenter: '' as AnyComponent, + PullRequestMergeState: '' as AnyComponent + }, + completion: { + PullRequestCategory: '' as Ref + }, + string: { + ConfigLabel: '' as IntlString, + ConfigDescription: '' as IntlString, + PRFile: '' as IntlString, + PRReview: '' as IntlString, + PRCommit: '' as IntlString, + PRDraft: '' as IntlString, + PRMergedAt: '' as IntlString, + PRClosedAt: '' as IntlString, + MergeCommitSHA: '' as IntlString, + GithubIssue: '' as IntlString, + Mergeable: '' as IntlString, + GithubUser: '' as IntlString, + + PullRequestMergeState: '' as IntlString, + PullRequestReviewDecision: '' as IntlString + }, + viewlet: { + PullRequests: '' as Ref + }, + functions: { + ShowForRepositoryOnly: '' as Resource<(spaces: Space[]) => Promise>, + UpdateIssue: '' as Resource + }, + ids: { + AssigneeNotification: '' as Ref, + GithubNotificationGroup: '' as Ref, + GitHubPullRequestChatMessageViewlet: '' as Ref + }, + category: { + Github: '' as Ref + } +}) diff --git a/services/github/model-github/tsconfig.json b/services/github/model-github/tsconfig.json new file mode 100644 index 0000000000..9765b6086d --- /dev/null +++ b/services/github/model-github/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/github/pod-github/.eslintrc.js b/services/github/pod-github/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/github/pod-github/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/github/pod-github/.gitignore b/services/github/pod-github/.gitignore new file mode 100644 index 0000000000..7665638e20 --- /dev/null +++ b/services/github/pod-github/.gitignore @@ -0,0 +1 @@ +markdowns \ No newline at end of file diff --git a/services/github/pod-github/.npmignore b/services/github/pod-github/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/github/pod-github/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/github/pod-github/Dockerfile b/services/github/pod-github/Dockerfile new file mode 100644 index 0000000000..88893a347c --- /dev/null +++ b/services/github/pod-github/Dockerfile @@ -0,0 +1,16 @@ + +FROM node:20 + +WORKDIR /usr/src/app + +RUN npm install --ignore-scripts=false --verbose bufferutil utf-8-validate @mongodb-js/zstd snappy --unsafe-perm +COPY bundle/bundle.js ./ + +RUN apt-get update +RUN apt-get install libjemalloc2 + +ENV LD_PRELOAD=libjemalloc.so.2 +ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true + +EXPOSE 3078 +CMD [ "node", "--inspect", "--async-stack-traces", "bundle.js" ] diff --git a/services/github/pod-github/Readme.md b/services/github/pod-github/Readme.md new file mode 100644 index 0000000000..a3dfd17950 --- /dev/null +++ b/services/github/pod-github/Readme.md @@ -0,0 +1,101 @@ +# Github App guidelines + +https://github.com/github/github-app-js-sample/tree/main + +## Developer resources + +### Forward github events to platform + +```typescript + smee -u https://smee.io/Oj0ZkULovroxbFC6 -t http://localhost:3500/api/webhook +``` + +### Shared application should be linked via `https://github.com/apps//installations/new?state=AB12t` + +https://docs.github.com/en/apps/sharing-github-apps/sharing-your-github-app + +## Instructions to setup GitHub integration for local testing + +### Register a new GitHub App + +Go to _Settings/Developer settings/GitHub Apps_ + +* Name: Any unique name. E.g. _XX_huly_dev_. Referred as _GITHUB_APP_ later in the document. +* Homepage URL: http://localhost:8080 +* Callback URL: http://localhost:8080/github +* Setup URL (optional): http://localhost:8080/github?op=installation +* Redirect on update: Checked + +#### New webhook + +Go to https://smee.io/ and click _Start a new channel_ + +* Use the provided Webhook Proxy URL as Webhook URL. Referred as _WEBHOOK_URL_ later in the document. +* Webhook secret is: `secret` +* Keep Webhook Active checked. + +#### Configure permissions for the app: + +* Commit statuses: _Read and write_ +* Contents: _Read and write_ +* Custom properties: _Read and write_ +* Discussions: _Read and write_ +* Issues: _Read and write_ +* Metadata: _Read-only_ +* Pages: _Read and write_ +* Projects: _Read and write_ +* Pull requests: _Read and write_ +* Webhooks: _Read and write_ + +#### Subscribe to events: + +* Issues +* Pull request +* Pull request review +* Pull request review comment +* Pull request review thread + +#### Final creation steps + +* Create the app. +* Generate a new client secret. Referred as _POD_GITHUB_CLIENT_SECRET_ later in the document. +* Create and download you private key file as well. Referred as _POD_GITHUB_PRIVATE_KEY_ later in the document. +* You'll be provided with your GitHub app ID. Referred as _POD_GITHUB_APPID_ later in the document. + +#### Forward webhook events to local server + +* Install smee client: + +``` +npm install --global smee-client +``` + +* To forward events (keep it up and running): + +``` +smee -u {WEBHOOK_URL} -t http://localhost:3500/api/webhook +``` +#### Update local config files + +##### .vscode/launch.json —> Debug Github integration + +* "APP_ID": "{POD_GITHUB_APPID}" <— Numeric ID of the new application +* "CLIENT_ID": "{POD_GITHUB_CLIENTID}" <—- Client ID from the new application +* "CLIENT_SECRET": "{POD_GITHUB_CLIENT_SECRET}" <—- Client Secret from the new application +* "PRIVATE_KEY": "{POD_GITHUB_PRIVATE_KEY}" <—- PK from the new application + +_Note: PK value format: "-----BEGIN RSA PRIVATE KEY-----\n {ACTUAL_KEY_WO_LINE_BREAKS}\n-----END RSA PRIVATE KEY-----_ + +##### dev/prod/public/config.json + +* "GITHUB_APP": “{GITHUB_APP}" <— Textual name of the new application +* "GITHUB_CLIENTID": “{POD_GITHUB_CLIENTID}” <— Client ID from the new application + +#### Usage + +* Run dev GitHub pod (Debug Github integration) in vscode +* Run dev server +* On localhost:8080 Go to Settings -> Integrations -> Github + * On the first tab authorise your GitHub + * On the second tab of the dialog install the application, select a GH repo and connect to an existing/create a new connected repo in the tracker. +* Enjoy! diff --git a/services/github/pod-github/config/rig.json b/services/github/pod-github/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/github/pod-github/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/github/pod-github/jest.config.js b/services/github/pod-github/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/github/pod-github/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/github/pod-github/package.json b/services/github/pod-github/package.json new file mode 100644 index 0000000000..0acf46a236 --- /dev/null +++ b/services/github/pod-github/package.json @@ -0,0 +1,173 @@ +{ + "name": "@hcengineering/pod-github", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "_phase:bundle": "rushx bundle", + "_phase:docker-build": "rushx docker:build", + "_phase:docker-staging": "rushx docker:staging", + "bundle": "mkdir -p bundle && esbuild src/index.ts --bundle --sourcemap=external --platform=node --outfile=bundle/bundle.js --log-level=error", + "docker:build": "../../../common/scripts/docker_build.sh hardcoreeng/github", + "docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/github staging", + "docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/github", + "docker:tbuild": "rush bundle --to @hcengineering/pod-github && docker build -t hardcoreeng/github . --platform=linux/amd64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/github", + "run-local": "cross-env APP_ID=$(cat ../../../../uberflow_private/appid) PRIVATE_KEY=\"$(cat ../../../../uberflow_private/private-key.pem)\" CLIENT_ID=$(cat ../../../../uberflow_private/client-id) CLIENT_SECRET=$(cat ../../../../uberflow_private/client-secret) SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000/ COLLABORATOR_URL=http://localhost:3078 COLLABORATOR_API_URL=http://localhost:3078 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ts-node src/index.ts", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "cross-env": "~7.0.3", + "@hcengineering/platform-rig": "^0.6.0", + "@types/node": "~20.11.16", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "esbuild": "^0.20.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "ts-node": "^10.8.0", + "typescript": "^5.3.3", + "@types/ws": "^8.5.11", + "smee-client": "^1.2.3", + "@octokit/webhooks-types": "^7.3.1", + "@types/markdown-it": "~13.0.0", + "graphql-binding": "^2.5.2", + "@types/express": "^4.17.13", + "@types/express-fileupload": "^1.1.7", + "@types/uuid": "^8.3.1", + "@types/cors": "^2.8.12", + "@types/body-parser": "~1.19.2", + "@types/compression": "~1.7.2", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5" + }, + "dependencies": { + "@hcengineering/core": "^0.6.32", + "@hcengineering/account": "^0.6.0", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/server-client": "^0.6.0", + "@hcengineering/server-token": "^0.6.11", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/server-storage": "^0.6.0", + "@hcengineering/mongo": "^0.6.1", + "@hcengineering/client": "^0.6.18", + "@hcengineering/chunter": "^0.6.20", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/client-resources": "^0.6.27", + "@hcengineering/tracker": "^0.6.24", + "@hcengineering/query": "^0.6.12", + "@hcengineering/text": "^0.6.5", + "@hcengineering/github": "^0.6.0", + "@hcengineering/attachment": "^0.6.14", + "ws": "^8.18.0", + "dotenv": "~16.0.0", + "octokit": "^3.1.1", + "@octokit/webhooks": "^12.0.3", + "@octokit/types": "^12.0.0", + "graphql": "^16.8.0", + "fast-equals": "^5.0.1", + "@hcengineering/minio": "^0.6.0", + "markdown-it": "^14.0.0", + "@tiptap/html": "^2.2.4", + "@tiptap/core": "^2.2.4", + "@tiptap/pm": "^2.2.4", + "@tiptap/starter-kit": "^2.2.4", + "@tiptap/extension-highlight": "^2.2.4", + "@tiptap/extension-placeholder": "^2.2.4", + "@tiptap/extension-mention": "^2.2.4", + "@tiptap/extension-typography": "^2.2.4", + "@tiptap/extension-link": "^2.2.4", + "@tiptap/suggestion": "^2.2.4", + "@tiptap/extension-task-list": "^2.2.4", + "@tiptap/extension-task-item": "^2.2.4", + "@tiptap/extension-collaboration": "^2.2.4", + "@tiptap/extension-collaboration-cursor": "^2.2.4", + "@tiptap/extension-code-block": "^2.2.4", + "@tiptap/extension-gapcursor": "^2.2.4", + "@tiptap/extension-heading": "^2.2.4", + "@tiptap/extension-table": "^2.2.4", + "@tiptap/extension-table-cell": "^2.2.4", + "@tiptap/extension-table-header": "^2.2.4", + "@tiptap/extension-table-row": "^2.2.4", + "@tiptap/extension-code": "^2.2.4", + "@tiptap/extension-bubble-menu": "^2.2.4", + "@hcengineering/task": "^0.6.20", + "express": "^4.19.2", + "cors": "^2.8.5", + "body-parser": "^1.20.2", + "mongodb": "^6.8.0", + "@hcengineering/setting": "^0.6.17", + "@hcengineering/preference": "^0.6.13", + "@hcengineering/login": "^0.6.12", + "@hcengineering/view": "^0.6.13", + "@hcengineering/recruit": "^0.6.29", + "@hcengineering/activity": "^0.6.0", + "@hcengineering/telegram": "^0.6.21", + "@hcengineering/workbench": "^0.6.16", + "@hcengineering/lead": "^0.6.0", + "@hcengineering/gmail": "^0.6.22", + "@hcengineering/inventory": "^0.6.11", + "@hcengineering/templates": "^0.6.11", + "@hcengineering/notification": "^0.6.23", + "@hcengineering/tags": "^0.6.16", + "@hcengineering/calendar": "^0.6.24", + "@hcengineering/board": "^0.6.19", + "@hcengineering/hr": "^0.6.19", + "@hcengineering/bitrix": "^0.6.52", + "@hcengineering/request": "^0.6.14", + "@hcengineering/drive": "^0.6.0", + "@hcengineering/support": "^0.6.5", + "@hcengineering/collaborator-client": "^0.6.4", + "@hcengineering/document": "^0.6.0", + "@hcengineering/login-assets": "^0.6.0", + "@hcengineering/view-assets": "^0.6.11", + "@hcengineering/task-assets": "^0.6.19", + "@hcengineering/chunter-assets": "^0.6.18", + "@hcengineering/recruit-assets": "^0.6.23", + "@hcengineering/setting-assets": "^0.6.15", + "@hcengineering/contact-assets": "^0.6.13", + "@hcengineering/activity-assets": "^0.6.3", + "@hcengineering/telegram-assets": "^0.6.0", + "@hcengineering/workbench-assets": "^0.6.14", + "@hcengineering/attachment-assets": "^0.6.11", + "@hcengineering/lead-assets": "^0.6.0", + "@hcengineering/gmail-assets": "^0.6.0", + "@hcengineering/inventory-assets": "^0.6.11", + "@hcengineering/templates-assets": "^0.6.11", + "@hcengineering/notification-assets": "^0.6.17", + "@hcengineering/preference-assets": "^0.6.0", + "@hcengineering/tags-assets": "^0.6.0", + "@hcengineering/calendar-assets": "^0.6.22", + "@hcengineering/tracker-assets": "^0.6.0", + "@hcengineering/board-assets": "^0.6.19", + "@hcengineering/hr-assets": "^0.6.19", + "@hcengineering/bitrix-assets": "^0.6.0", + "@hcengineering/request-assets": "^0.6.0", + "@hcengineering/drive-assets": "^0.6.0", + "@hcengineering/support-assets": "^0.6.5", + "@hcengineering/github-assets": "^0.6.0", + "@hcengineering/document-assets": "^0.6.0", + "@hcengineering/time": "^0.6.0", + "@hcengineering/server-guest-resources": "^0.6.0", + "@hcengineering/analytics": "^0.6.0", + "@hcengineering/analytics-service": "^0.6.0" + } +} diff --git a/services/github/pod-github/src/account.ts b/services/github/pod-github/src/account.ts new file mode 100644 index 0000000000..24e8823744 --- /dev/null +++ b/services/github/pod-github/src/account.ts @@ -0,0 +1,24 @@ +import { ClientWorkspaceInfo } from '@hcengineering/account' +import config from './config' + +/** + * @public + */ +export async function getWorkspaceInfo (token: string): Promise { + const accountsUrl = config.AccountsURL + const workspaceInfo = await ( + await fetch(accountsUrl, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + method: 'getWorkspaceInfo', + params: [] + }) + }) + ).json() + + return workspaceInfo.result as ClientWorkspaceInfo +} diff --git a/services/github/pod-github/src/client.ts b/services/github/pod-github/src/client.ts new file mode 100644 index 0000000000..789761d333 --- /dev/null +++ b/services/github/pod-github/src/client.ts @@ -0,0 +1,50 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// + +import client, { ClientSocket } from '@hcengineering/client' +import clientResources from '@hcengineering/client-resources' +import { Client, ClientConnectEvent } from '@hcengineering/core' +import { setMetadata } from '@hcengineering/platform' +import { getTransactorEndpoint } from '@hcengineering/server-client' +import serverToken, { generateToken } from '@hcengineering/server-token' +import WebSocket from 'ws' +import config from './config' + +/** + * @public + */ +export async function createPlatformClient ( + workspace: string, + productId: string, + timeout: number, + reconnect?: (event: ClientConnectEvent) => void +): Promise { + setMetadata(client.metadata.ClientSocketFactory, (url) => { + return new WebSocket(url, { + headers: { + 'User-Agent': config.ServiceID + } + }) as never as ClientSocket + }) + + setMetadata(serverToken.metadata.Secret, config.ServerSecret) + const token = generateToken( + config.SystemEmail, + { + name: workspace, + productId + }, + { mode: 'github' } + ) + setMetadata(client.metadata.ConnectionTimeout, timeout) + const endpoint = await getTransactorEndpoint(token) + const connection = await ( + await clientResources() + ).function.GetClient(token, endpoint, { + onConnect: reconnect + }) + + return connection +} diff --git a/services/github/pod-github/src/collaborator.ts b/services/github/pod-github/src/collaborator.ts new file mode 100644 index 0000000000..19d5ab9226 --- /dev/null +++ b/services/github/pod-github/src/collaborator.ts @@ -0,0 +1,17 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// + +import { CollaboratorClient, getClient as getCollaboratorClient } from '@hcengineering/collaborator-client' +import { Hierarchy, WorkspaceId } from '@hcengineering/core' +import { generateToken } from '@hcengineering/server-token' +import config from './config' + +/** + * @public + */ +export function createCollaboratorClient (hierarchy: Hierarchy, workspaceId: WorkspaceId): CollaboratorClient { + const token = generateToken(config.SystemEmail, workspaceId, { mode: 'github' }) + return getCollaboratorClient(hierarchy, workspaceId, token, config.CollaboratorURL) +} diff --git a/services/github/pod-github/src/config.ts b/services/github/pod-github/src/config.ts new file mode 100644 index 0000000000..68ce448437 --- /dev/null +++ b/services/github/pod-github/src/config.ts @@ -0,0 +1,124 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +import { systemAccountEmail } from '@hcengineering/core' + +interface Config { + AccountsURL: string + ServiceID: string + ServerSecret: string + SystemEmail: string + FrontURL: string + + // '*' means all workspaces + AllowedWorkspaces: string[] + AppID: string + ClientID: string + ClientSecret: string + PrivateKey: string + WebhookSecret: string + EnterpriseHostname: string + Port: number + + MongoURL: string + ConfigurationDB: string + + CollaboratorURL: string + + ProductID: string + + BotName: string + + SentryDSN: string + BrandingPath: string +} + +const envMap: { [key in keyof Config]: string } = { + AccountsURL: 'ACCOUNTS_URL', + ServiceID: 'SERVICE_ID', + ServerSecret: 'SERVER_SECRET', + SystemEmail: 'SYSTEM_EMAIL', + FrontURL: 'FRONT_URL', + + AppID: 'APP_ID', + ClientID: 'CLIENT_ID', + ClientSecret: 'CLIENT_SECRET', + PrivateKey: 'PRIVATE_KEY', + WebhookSecret: 'WEBHOOK_SECRET', + EnterpriseHostname: 'ENTERPRISE_HOSTNAME', + Port: 'PORT', + AllowedWorkspaces: 'ALLOWED_WORKSPACES', + BotName: 'BOT_NAME', + + MongoURL: 'MONGO_URL', + ConfigurationDB: 'MONGO_DB', + + CollaboratorURL: 'COLLABORATOR_API_URL', + + ProductID: 'PRODUCT_ID', + + SentryDSN: 'SENTRY_DSN', + BrandingPath: 'BRANDING_PATH' +} + +const required: Array = [ + 'AccountsURL', + 'ServerSecret', + 'ServiceID', + 'SystemEmail', + 'FrontURL', + 'AppID', + 'ClientID', + 'ClientSecret', + 'PrivateKey', + + 'MongoURL', + 'ConfigurationDB', + + 'CollaboratorURL', + + 'ProductID', + 'BotName' +] + +const config: Config = (() => { + const params: Partial = { + AccountsURL: process.env[envMap.AccountsURL], + ServerSecret: process.env[envMap.ServerSecret], + ServiceID: process.env[envMap.ServiceID] ?? 'github-service', + SystemEmail: process.env[envMap.SystemEmail] ?? systemAccountEmail, + AllowedWorkspaces: process.env[envMap.AllowedWorkspaces]?.split(',') ?? ['*'], + FrontURL: process.env[envMap.FrontURL] ?? '', + + AppID: process.env[envMap.AppID], + ClientID: process.env[envMap.ClientID], + ClientSecret: process.env[envMap.ClientSecret], + // https://github.com/octokit/auth-app.js/issues/465 + PrivateKey: process.env[envMap.PrivateKey]?.replace(/\\n/g, '\n'), + WebhookSecret: process.env[envMap.WebhookSecret] ?? 'secret', + EnterpriseHostname: process.env[envMap.EnterpriseHostname], + Port: parseInt(process.env[envMap.Port] ?? '3500'), + BotName: process.env[envMap.BotName] ?? 'dev[bot]', + + MongoURL: process.env[envMap.MongoURL], + ConfigurationDB: process.env[envMap.ConfigurationDB] ?? '%github', + + CollaboratorURL: process.env[envMap.CollaboratorURL], + + ProductID: process.env[envMap.ProductID] ?? '', + + SentryDSN: process.env[envMap.SentryDSN], + BrandingPath: process.env[envMap.BrandingPath] ?? '' + } + + const missingEnv = required.filter((key) => params[key] === undefined).map((key) => envMap[key]) + + if (missingEnv.length > 0) { + throw Error(`Missing env variables: ${missingEnv.join(', ')}`) + } + + return params as Config +})() + +export default config diff --git a/services/github/pod-github/src/index.ts b/services/github/pod-github/src/index.ts new file mode 100644 index 0000000000..fa038ef824 --- /dev/null +++ b/services/github/pod-github/src/index.ts @@ -0,0 +1,59 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +import { MeasureMetricsContext, metricsToString, newMetrics } from '@hcengineering/core' +import { SplitLogger, configureAnalytics } from '@hcengineering/analytics-service' +import { writeFile } from 'fs/promises' +import { join } from 'path' +import config from './config' +import { start } from './server' +import { Analytics } from '@hcengineering/analytics' +import { loadBrandingMap } from '@hcengineering/server-core' + +// Load and inc startID, to have easy logs. + +const metricsContext = new MeasureMetricsContext( + 'github', + {}, + {}, + newMetrics(), + new SplitLogger('github-service', { + root: join(process.cwd(), 'logs'), + enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true' + }) +) + +configureAnalytics(config.SentryDSN, config) +Analytics.setTag('application', 'github-service') + +let oldMetricsValue = '' + +const intTimer = setInterval(() => { + const val = metricsToString(metricsContext.metrics, 'Github', 140) + if (val !== oldMetricsValue) { + oldMetricsValue = val + void writeFile('metrics.txt', val).catch((err) => { + console.error(err) + }) + } +}, 30000) + +void start(metricsContext, loadBrandingMap(config.BrandingPath)) + +const onClose = (): void => { + clearInterval(intTimer) + metricsContext.info('Closed') +} + +process.on('uncaughtException', (e) => { + metricsContext.error('UncaughtException', { error: e }) +}) + +process.on('unhandledRejection', (reason, promise) => { + metricsContext.error('Unhandled Rejection at:', { promise, reason }) +}) + +process.on('SIGINT', onClose) +process.on('SIGTERM', onClose) +process.on('exit', onClose) diff --git a/services/github/pod-github/src/loaders.ts b/services/github/pod-github/src/loaders.ts new file mode 100644 index 0000000000..31fb168e35 --- /dev/null +++ b/services/github/pod-github/src/loaders.ts @@ -0,0 +1,96 @@ +import { coreId } from '@hcengineering/core' + +import { activityId } from '@hcengineering/activity' +import { attachmentId } from '@hcengineering/attachment' +import { bitrixId } from '@hcengineering/bitrix' +import { boardId } from '@hcengineering/board' +import { calendarId } from '@hcengineering/calendar' +import { chunterId } from '@hcengineering/chunter' +import { contactId } from '@hcengineering/contact' +import { driveId } from '@hcengineering/drive' +import { gmailId } from '@hcengineering/gmail' +import { hrId } from '@hcengineering/hr' +import { inventoryId } from '@hcengineering/inventory' +import { leadId } from '@hcengineering/lead' +import { loginId } from '@hcengineering/login' +import { notificationId } from '@hcengineering/notification' +import { preferenceId } from '@hcengineering/preference' +import { recruitId } from '@hcengineering/recruit' +import { requestId } from '@hcengineering/request' +import { settingId } from '@hcengineering/setting' +import { supportId } from '@hcengineering/support' +import { tagsId } from '@hcengineering/tags' +import { taskId } from '@hcengineering/task' +import { telegramId } from '@hcengineering/telegram' +import { templatesId } from '@hcengineering/templates' +import { trackerId } from '@hcengineering/tracker' +import { viewId } from '@hcengineering/view' +import { workbenchId } from '@hcengineering/workbench' +import { documentId } from '@hcengineering/document' +import { githubId } from '@hcengineering/github' + +import activityEn from '@hcengineering/activity-assets/lang/en.json' +import attachmentEn from '@hcengineering/attachment-assets/lang/en.json' +import bitrixEn from '@hcengineering/bitrix-assets/lang/en.json' +import boardEn from '@hcengineering/board-assets/lang/en.json' +import calendarEn from '@hcengineering/calendar-assets/lang/en.json' +import chunterEn from '@hcengineering/chunter-assets/lang/en.json' +import contactEn from '@hcengineering/contact-assets/lang/en.json' +import coreEng from '@hcengineering/core/lang/en.json' +import driveEn from '@hcengineering/drive-assets/lang/en.json' +import gmailEn from '@hcengineering/gmail-assets/lang/en.json' +import hrEn from '@hcengineering/hr-assets/lang/en.json' +import inventoryEn from '@hcengineering/inventory-assets/lang/en.json' +import leadEn from '@hcengineering/lead-assets/lang/en.json' +import loginEng from '@hcengineering/login-assets/lang/en.json' +import platformEng from '@hcengineering/platform/lang/en.json' +import notificationEn from '@hcengineering/notification-assets/lang/en.json' +import { addStringsLoader, platformId } from '@hcengineering/platform' +import preferenceEn from '@hcengineering/preference-assets/lang/en.json' +import recruitEn from '@hcengineering/recruit-assets/lang/en.json' +import requestEn from '@hcengineering/request-assets/lang/en.json' +import settingEn from '@hcengineering/setting-assets/lang/en.json' +import supportEn from '@hcengineering/support-assets/lang/en.json' +import tagsEn from '@hcengineering/tags-assets/lang/en.json' +import taskEn from '@hcengineering/task-assets/lang/en.json' +import telegramEn from '@hcengineering/telegram-assets/lang/en.json' +import templatesEn from '@hcengineering/templates-assets/lang/en.json' +import trackerEn from '@hcengineering/tracker-assets/lang/en.json' +import viewEn from '@hcengineering/view-assets/lang/en.json' +import workbenchEn from '@hcengineering/workbench-assets/lang/en.json' +import documentEn from '@hcengineering/document-assets/lang/en.json' +import githubEn from '@hcengineering/github-assets/lang/en.json' + +export function registerLoaders (): void { + addStringsLoader(coreId, async (lang: string) => coreEng) + addStringsLoader(loginId, async (lang: string) => loginEng) + addStringsLoader(platformId, async (lang: string) => platformEng) + + addStringsLoader(taskId, async (lang: string) => taskEn) + addStringsLoader(viewId, async (lang: string) => viewEn) + addStringsLoader(chunterId, async (lang: string) => chunterEn) + addStringsLoader(attachmentId, async (lang: string) => attachmentEn) + addStringsLoader(contactId, async (lang: string) => contactEn) + addStringsLoader(recruitId, async (lang: string) => recruitEn) + addStringsLoader(activityId, async (lang: string) => activityEn) + addStringsLoader(settingId, async (lang: string) => settingEn) + addStringsLoader(telegramId, async (lang: string) => telegramEn) + addStringsLoader(leadId, async (lang: string) => leadEn) + addStringsLoader(gmailId, async (lang: string) => gmailEn) + addStringsLoader(workbenchId, async (lang: string) => workbenchEn) + addStringsLoader(inventoryId, async (lang: string) => inventoryEn) + addStringsLoader(templatesId, async (lang: string) => templatesEn) + addStringsLoader(notificationId, async (lang: string) => notificationEn) + addStringsLoader(tagsId, async (lang: string) => tagsEn) + addStringsLoader(calendarId, async (lang: string) => calendarEn) + addStringsLoader(trackerId, async (lang: string) => trackerEn) + addStringsLoader(boardId, async (lang: string) => boardEn) + addStringsLoader(preferenceId, async (lang: string) => preferenceEn) + addStringsLoader(hrId, async (lang: string) => hrEn) + addStringsLoader(documentId, async (lang: string) => documentEn) + addStringsLoader(bitrixId, async (lang: string) => bitrixEn) + addStringsLoader(requestId, async (lang: string) => requestEn) + addStringsLoader(supportId, async (lang: string) => supportEn) + addStringsLoader(githubId, async (lang: string) => githubEn) + addStringsLoader(driveId, async (lang: string) => driveEn) +} diff --git a/services/github/pod-github/src/markdown/__tests__/markdown.json.gz b/services/github/pod-github/src/markdown/__tests__/markdown.json.gz new file mode 100644 index 0000000000..7e8539ad3f Binary files /dev/null and b/services/github/pod-github/src/markdown/__tests__/markdown.json.gz differ diff --git a/services/github/pod-github/src/markdown/__tests__/markup.test.ts b/services/github/pod-github/src/markdown/__tests__/markup.test.ts new file mode 100644 index 0000000000..39d162cb99 --- /dev/null +++ b/services/github/pod-github/src/markdown/__tests__/markup.test.ts @@ -0,0 +1,112 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/* eslint-env jest */ +import { setMetadata } from '@hcengineering/platform' +import serverCore from '@hcengineering/server-core' +import { jsonToHTML, htmlToJSON } from '@hcengineering/text' +import { markupToMarkdown, markdownToMarkup, parseMessageMarkdown, serializeMessage } from '..' +import { appendGuestLinkToModel, stripGuestLink } from '../../sync/guest' +import { GithubKit } from '../extensions' + +const refUrl: string = 'ref://' +const imageUrl: string = 'http://localhost' +const guestUrl: string = 'http://localhost:8080/guest' + +const extensions = [GithubKit] + +describe('server', () => { + it('embedded markup parsing', () => { + const markdown = `test5 + + Screenshot 2024-01-22 at 10 39 21 + + Screenshot 2024-01-22 at 10 39 26 + + ` + const json = parseMessageMarkdown(markdown, refUrl, imageUrl, guestUrl) + + const html = jsonToHTML(json, extensions) + + const json2 = htmlToJSON(html, extensions) + const newMarkdown = serializeMessage(json2, refUrl, imageUrl) + + console.log(newMarkdown) + }) + it('add html link', () => { + const markdown = `test5 + +Screenshot 2024-01-22 at 10 39 21 + +Screenshot 2024-01-22 at 10 39 26 + + + View at Huly TSK-1023 + +` + + const json = parseMessageMarkdown(markdown, refUrl, imageUrl, guestUrl) + console.log(json) + const html = jsonToHTML(json, extensions) + const json2 = htmlToJSON(html, extensions) + const newMarkdown = serializeMessage(json2, refUrl, imageUrl) + console.log(newMarkdown) + }) + it('check parsing with sub', async () => { + const markdown = + 'qwe4 qwe6\n\n![](http://localhost:8080/files?workspace=github&file=76e25453-186d-46e5-b9cd-d296a5342ce2&width=997)qwe 77\n\nzzz2 3\n\nView in Huly TSK-50' + const json = parseMessageMarkdown(markdown, refUrl, imageUrl, guestUrl) + setMetadata(serverCore.metadata.FrontUrl, 'http://localhost:8080') + await stripGuestLink(json) + const newMarkdown = serializeMessage(json, refUrl, imageUrl) + console.log(json, newMarkdown) + expect(newMarkdown).toBe( + 'qwe4 qwe6\n\n![](http://localhost76e25453-186d-46e5-b9cd-d296a5342ce2&width=997)qwe 77\n\nzzz2 3' + ) + }) + it('code block', async () => { + const markdown = '```bash\n2\nbash qwe\n3\nbase qwe2\n4\nbaseh qwe4\n5\n```' + const markup = markdownToMarkup(markdown) + const markdown2 = await markupToMarkdown(markup) + expect(markdown2).toBe(markdown) + }) + + it('block image', async () => { + const markdown = 'qwerty\nimage' + const markup = markdownToMarkup(markdown) + const markdown2 = await markupToMarkdown(markup) + expect(markdown2).toBe(markdown) + }) + + it('inline image', async () => { + const markdown = '* line 1\n* line 2\n image' + const markup = markdownToMarkup(markdown) + const markdown2 = await markupToMarkdown(markup) + expect(markdown2).toBe(markdown) + }) + + it('test view in serialization', async () => { + const markdown = '```bash\n2\nbash qwe\n3\nbase qwe2\n4\nbaseh qwe4\n5\n```' + const json = parseMessageMarkdown(markdown, refUrl, imageUrl, guestUrl) + appendGuestLinkToModel(json, 'http://test.com', 'TSK-1235') + const serializedMarkdown = serializeMessage(json, refUrl, imageUrl) + await stripGuestLink(json) + const serializedMarkdown2 = serializeMessage(json, refUrl, imageUrl) + expect(serializedMarkdown).toBe( + '```bash\n2\nbash qwe\n3\nbase qwe2\n4\nbaseh qwe4\n5\n```\n\nHuly®: TSK-1235' + ) + expect(serializedMarkdown2).toBe('```bash\n2\nbash qwe\n3\nbase qwe2\n4\nbaseh qwe4\n5\n```') + }) +}) diff --git a/services/github/pod-github/src/markdown/__tests__/testMarkdowns.test.ts_ b/services/github/pod-github/src/markdown/__tests__/testMarkdowns.test.ts_ new file mode 100644 index 0000000000..095a32f2b6 --- /dev/null +++ b/services/github/pod-github/src/markdown/__tests__/testMarkdowns.test.ts_ @@ -0,0 +1,81 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/* eslint-env jest */ +import { htmlToJSON, jsonToHTML } from '@hcengineering/text' +import { parseMessageMarkdown, serializeMessage } from '..' +import { defaultExtensions } from '../extensions' + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs' +import { dirname, join } from 'path' +import { gunzipSync } from 'zlib' +import { calcSørensenDiceCoefficient, isMarkdownsEquals } from '../compare' + +const refUrl: string = 'ref://' +const imageUrl: string = 'http://localhost' + +const markdownSamples = JSON.parse(gunzipSync(readFileSync('./src/markdown/__tests__/markdown.json.gz')).toString()) + +const testStrings = Array.from((markdownSamples)).reduce((decodedStrings: { repoName: string, markdownSource: string, markdown: string }[], { markdownEncoded, markdownEncoding, repoName, markdownSource }: any) => { + if (markdownEncoding === 'base64') { + decodedStrings.push({ repoName, markdownSource, markdown: atob(markdownEncoded) }) + } + + return decodedStrings +}, []) + +describe('server', () => { + it('test-all', () => { + let i = 0 + const dta = testStrings + let errors: number = 0 + let minK = 100 + let minKi = 0 + const result = [] + for (const sample of dta) { + try { + const fileName = join('src', 'markdown', '__tests__', 'markdowns', `${i}${sample.repoName}`, sample.markdownSource) + + const json = parseMessageMarkdown(sample.markdown, refUrl, imageUrl) + + const html = jsonToHTML(json, defaultExtensions) + + const json2 = htmlToJSON(html, defaultExtensions) + const newMarkdown = serializeMessage(json2, refUrl, imageUrl) + const k = calcSørensenDiceCoefficient(sample.markdown, newMarkdown) * 100 + minK = Math.min(k, minK) + if (minK === k) { + minKi = i + } + const equals = isMarkdownsEquals(sample.markdown, newMarkdown) + if (k < 50) { + if (!existsSync(dirname(fileName))) { + mkdirSync(dirname(fileName), { recursive: true }) + } + writeFileSync(fileName + '_source.md', sample.markdown) + writeFileSync(fileName + '_target.md', newMarkdown) + } + result.push(`${fileName}: ${k} ${equals}`) + } catch (e) { + errors++ + // console.error(e) + } + i++ + } + console.log('Result', result.join('\n')) + console.log('MinK:', minK, minKi) + console.log('Errors:', errors) + }) +}) diff --git a/services/github/pod-github/src/markdown/__tests__/textmodel.test.ts b/services/github/pod-github/src/markdown/__tests__/textmodel.test.ts new file mode 100644 index 0000000000..070bff0683 --- /dev/null +++ b/services/github/pod-github/src/markdown/__tests__/textmodel.test.ts @@ -0,0 +1,964 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/* eslint-env jest */ +import { + MarkupMarkType, + MarkupNode, + MarkupNodeType, + jsonToMarkup, + MarkdownState, + traverseAllMarks, + traverseMarkupNode +} from '@hcengineering/text' +import { markdownToMarkup, markupToMarkdown, parseMessageMarkdown, serializeMessage } from '..' + +describe('server', () => { + it('todos serialize and back', async () => { + const markdown = `# Contribution checklist + +## Brief description + +## Checklist + +* [ ] - Are screenshots added to PR if applicable? +* [x] - Does the code work as expected and all the requirements in the task are covered? +* [x] - Are all new user-facing texts added through the translations mechanism? +* [x] - Are all of the requirements in the task well tested? +* [x] - Tested in Chrome? +* [x] - Tested in Safari? +* [x] - Have you checked the new code for typos, TODOs, commented LOCs, debug code, etc.? +* [x] - Ensure your branch is up to date with the \`main\` branch +* [x] - Is there any redundant or duplicate code? +* [x] - Are required links added to PR? +* [x] - Is the new code well documented? + +## Related issues + +A list of closed updated issues` + const markup = markdownToMarkup(markdown) + const markdownAgain = await markupToMarkdown(markup) + expect(markdownAgain).toBe(markdown) + }) + + it('html to markdown with links', async () => { + const data = jsonToMarkup({ + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'As a part of the Platform it would be nice to have an ' + }, + { + type: MarkupNodeType.text, + text: 'effect', + marks: [{ type: MarkupMarkType.code, attrs: {} }] + }, + { + type: MarkupNodeType.text, + text: ' subsystem. Something similar to ' + }, + { + type: MarkupNodeType.text, + text: 'https://www.effect.website', + marks: [ + { + type: MarkupMarkType.link, + attrs: { + href: 'https://www.effect.website' + } + } + ] + } + ] + } + ] + }) + const markDownData = await markupToMarkdown(data) + expect(markDownData).toEqual( + 'As a part of the Platform it would be nice to have an `effect` subsystem. Something similar to ' + ) + }) + + it('html to markdown with links-2', async () => { + const data = jsonToMarkup({ + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'As a part of the Platform it would be nice to have an ' + }, + { + type: MarkupNodeType.text, + text: 'effect', + marks: [{ type: MarkupMarkType.code, attrs: {} }] + }, + { + type: MarkupNodeType.text, + text: ' subsystem. Something similar to ' + }, + { + type: MarkupNodeType.text, + text: 'Effects', + marks: [ + { + type: MarkupMarkType.link, + attrs: { + href: 'https://www.effect.website' + } + } + ] + } + ] + } + ] + }) + const markDownData = await markupToMarkdown(data) + expect(markDownData).toEqual( + 'As a part of the Platform it would be nice to have an `effect` subsystem. Something similar to [Effects](https://www.effect.website)' + ) + }) + + it('markup to markdown', async () => { + const data = jsonToMarkup({ + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.reference, + attrs: { + id: '629d8b615bdc96430ced15a0', + objectclass: 'contact:class:Person', + label: 'Sobolev Andrey' + } + }, + { + type: MarkupNodeType.text, + text: ' ' + }, + { + type: MarkupNodeType.reference, + attrs: { + id: '64db123e602161ac4482475c', + objectclass: 'github:class:GithubPullRequest', + label: 'UBERF-16' + } + }, + { + type: MarkupNodeType.text, + text: ' link test' + } + ] + } + ] + }) + const data2 = + '[Sobolev Andrey](https://front.hc.engineering/open/?workspace=workspace&_class=contact%3Aclass%3APerson&_id=629d8b615bdc96430ced15a0&label=Sobolev%20Andrey) [UBERF-16](https://front.hc.engineering/open/?workspace=workspace&_class=github%3Aclass%3AGithubPullRequest&_id=64db123e602161ac4482475c&label=UBERF-16) link test' + const html = await markupToMarkdown(data, 'https://front.hc.engineering/open/?workspace=workspace') + expect(html).toEqual(data2) + }) + + it('markdown to markup', () => { + const data = + '[Sobolev Andrey](https://front.hc.engineering/open/?workspace=workspace&_class=contact%3Aclass%3APerson&_id=629d8b615bdc96430ced15a0&label=Sobolev%20Andrey) [UBERF-16](https://front.hc.engineering/open/?workspace=workspace&_class=github%3Aclass%3AGithubPullRequest&_id=64db123e602161ac4482475c&label=UBERF-16) link test' + const data2 = jsonToMarkup({ + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.reference, + content: [ + { + type: MarkupNodeType.text, + text: 'Sobolev Andrey', + marks: [] + } + ], + attrs: { + label: 'Sobolev Andrey', + id: '629d8b615bdc96430ced15a0', + objectclass: 'contact:class:Person' + } + }, + { + type: MarkupNodeType.text, + text: ' ', + marks: [] + }, + { + type: MarkupNodeType.reference, + content: [ + { + type: MarkupNodeType.text, + text: 'UBERF-16', + marks: [] + } + ], + attrs: { + label: 'UBERF-16', + id: '64db123e602161ac4482475c', + objectclass: 'github:class:GithubPullRequest' + } + }, + { + type: MarkupNodeType.text, + text: ' link test', + marks: [] + } + ] + } + ] + }) + const markup = markdownToMarkup(data, 'https://front.hc.engineering/open/?workspace=workspace') + expect(markup).toEqual(data2) + }) + + it('test conversion', async () => { + const data = jsonToMarkup({ + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'Yahoo', + marks: [{ type: MarkupMarkType.bold, attrs: {} }] + }, + { + type: MarkupNodeType.text, + text: ' something is strange' + }, + { + type: MarkupNodeType.hard_break + }, + { + type: MarkupNodeType.text, + text: 'some more line 2' + } + ] + }, + { + type: MarkupNodeType.heading, + attrs: { + level: 2 + }, + content: [ + { + type: MarkupNodeType.text, + text: 'Header' + } + ] + }, + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'Test 3' + }, + { + type: MarkupNodeType.hard_break + }, + { + type: MarkupNodeType.hard_break + } + ] + }, + { + type: MarkupNodeType.bullet_list, + content: [ + { + type: MarkupNodeType.list_item, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'qeqwe' + } + ] + } + ] + }, + { + type: MarkupNodeType.list_item, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'qwewqe' + } + ] + } + ] + } + ] + }, + { + type: MarkupNodeType.paragraph + }, + { + type: MarkupNodeType.taskList, + content: [ + { + type: MarkupNodeType.taskItem, + attrs: { + checked: 'false' + }, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'qweqwewq' + } + ] + } + ] + } + ] + } + ] + }) + const markdown = await markupToMarkdown(data, 'https://front.hc.engineering/open/?workspace=workspace') + const markup = markdownToMarkup(markdown, 'https://front.hc.engineering/open/?workspace=workspace') + console.log(markdown, markup) + }) + + it('Check parsing header', () => { + const t1 = '# This is header' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + it('Check parsing bullets', () => { + const t1 = '* Section A\n Some text\n* Section B\n Some more text' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + it('Check parsing bullets-2', () => { + const t1 = '* Section A\n* Some section2' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual('* Section A\n* Some section2') + }) + + it('Check ordered list', () => { + const t1 = '1. Section A\n Some text\n2. Section B\n Some more text' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].type).toEqual(MarkupNodeType.ordered_list) + expect(msg.content?.[0].content?.[0].type).toEqual(MarkupNodeType.list_item) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + + it('Check styles', () => { + const t1 = '**BOLD _ITALIC_ BOLD**' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual('**BOLD *ITALIC* BOLD**') + }) + + it('Check styles-2', () => { + const t1 = '**BOLD *ITALIC* BOLD**' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + + it('Check styles-3', () => { + const t1 = 'Some *EM **MORE EM***' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].type).toEqual(MarkupNodeType.paragraph) + expect(msg.content?.[0].content?.length).toEqual(3) + expect(msg.content?.[0].content?.[2]?.marks?.length).toEqual(2) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + + it('Check hardbreaks', () => { + const t1 = 'foo\\\nbaz' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].content?.[0].type).toEqual(MarkupNodeType.text) + expect(msg.content?.[0].content?.[1].type).toEqual(MarkupNodeType.hard_break) + expect(msg.content?.[0].content?.[2].type).toEqual(MarkupNodeType.text) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual('foo\\\nbaz') + }) + + it('Check hardbreaks with spaces', () => { + const t1 = 'foo \nbaz' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].content?.[0].type).toEqual(MarkupNodeType.text) + expect(msg.content?.[0].content?.[1].type).toEqual(MarkupNodeType.hard_break) + expect(msg.content?.[0].content?.[2].type).toEqual(MarkupNodeType.text) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual('foo\\\nbaz') + }) + + it('Check softbreaks', () => { + const t1 = 'foo\nbaz' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].content?.[0].type).toEqual(MarkupNodeType.text) + expect(msg.content?.[0].content?.[0].text).toEqual('foo\nbaz') + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual('foo\nbaz') + }) + + it('Check softbreaks with spaces', () => { + const t1 = 'foo \n baz' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].content?.[0].type).toEqual(MarkupNodeType.text) + expect(msg.content?.[0].content?.[0].text).toEqual('foo\nbaz') + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual('foo\nbaz') + }) + + it('Check images', () => { + const t1 = 'Some text\nsome text ![This is Alt](http://url/a.png "This is title") Some text' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].content?.[1].type).toEqual(MarkupNodeType.image) + expect(msg.content?.[0].content?.[1].attrs?.src).toEqual('http://url/a.png') + expect(msg.content?.[0].content?.[1].attrs?.title).toEqual('This is title') + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + + it('Check block quote', () => { + const t1 = '> Some quoted text\nand some more text' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].type).toEqual(MarkupNodeType.blockquote) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual('> Some quoted text\n> and some more text') + }) + + it('Check block quote-2', () => { + const t1 = '> Some quoted text\n> and some more text' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].type).toEqual(MarkupNodeType.blockquote) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + it('Check block quote-3', () => { + const t1 = '> Some quoted text\n\nand some more text' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].type).toEqual(MarkupNodeType.blockquote) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + + it('Check code block', () => { + const t1 = "```\n# code block\nprint '3 backticks or'\nprint 'indent 4 spaces'\n```" + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].type).toEqual(MarkupNodeType.code_block) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + + it('Check inline block', () => { + const t1 = 'Hello `Some code` block' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].type).toEqual(MarkupNodeType.paragraph) + expect(msg.content?.[0].content?.length).toEqual(3) + expect(msg.content?.[0].content?.[1].marks?.[0].type).toEqual(MarkupMarkType.code) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + + it('Check underline heading rule', () => { + const t1 = 'Hello\n---\nSome text' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual('## Hello\n\nSome text') + }) + + it('Check horizontal line', () => { + const t1 = 'Hello\n\n---\n\nSome text' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.length).toEqual(3) + expect(msg.content?.[1].type).toEqual(MarkupNodeType.horizontal_rule) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + + it('Check big inline block', () => { + const t1 = 'Hello ```Some code``` block' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].type).toEqual(MarkupNodeType.paragraph) + expect(msg.content?.[0].content?.length).toEqual(3) + expect(msg.content?.[0].content?.[1].marks?.[0].type).toEqual(MarkupMarkType.code) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual('Hello `Some code` block') + }) + it('Check code block language', () => { + const t1 = "```typescript\n# code block\nprint '3 backticks or'\nprint 'indent 4 spaces'\n```" + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].type).toEqual(MarkupNodeType.code_block) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + + it('Check link', () => { + const t1 = 'Some text [Link Alt](http://a.com) some more text' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + it('Check link bold', () => { + const t1 = '**[link](foo) is bold**"' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].content?.[1].marks?.[0]?.type).toEqual(MarkupMarkType.bold) + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + + it('Check overlapping inline marks', () => { + const t1 = 'This is **strong *emphasized text with `code` in* it**' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].content?.length).toEqual(6) + + const md = serializeMessage(msg, 'ref://', 'http://') + + expect(md).toEqual(t1) + }) + it('Check emph url', () => { + const t1 = 'Link to **' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg.type).toEqual(MarkupNodeType.doc) + expect(msg.content?.[0].content?.length).toEqual(2) + + const md = serializeMessage(msg, 'ref://', 'http://') + expect(md).toEqual('Link to **') + }) + it('check header hard_break serialize', () => { + const doc: MarkupNode = { + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: '# Hello' + }, + { + type: MarkupNodeType.hard_break + }, + { + type: MarkupNodeType.text, + text: 'World' + } + ] + } + ] + } + const md = serializeMessage(doc, 'ref://', 'http://') + expect(md).toEqual('# Hello\\\nWorld') + }) + it('Check inline html - 1', () => { + const t1 = '' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg).toEqual({ + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: '*foo*', + marks: [ + { + type: MarkupMarkType.link, + attrs: { + href: 'bar', + class: 'cursor-pointer', + rel: 'noopener noreferrer', + target: '_blank' + } + } + ] + } + ] + } + ] + }) + }) + it('Check inline html - 2', () => { + const t1 = '

hello

\n

world

' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg).toEqual({ + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.heading, + attrs: { level: 1 }, + content: [ + { + type: MarkupNodeType.text, + text: 'hello' + } + ] + }, + { + type: MarkupNodeType.heading, + attrs: { level: 2 }, + content: [ + { + type: MarkupNodeType.text, + text: 'world' + } + ] + } + ] + }) + }) + + it('Check inline html - 3', () => { + const t1 = '* line 1\n* line 2\n image' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + expect(msg).toEqual({ + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.bullet_list, + content: [ + { + type: MarkupNodeType.list_item, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'line 1', + marks: [] + } + ] + } + ] + }, + { + type: MarkupNodeType.list_item, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'line 2\n', + marks: [] + }, + { + type: MarkupNodeType.image, + attrs: { + src: 'http://example.com/image', + alt: 'image', + width: 320, + align: null, + height: null, + title: null, + 'file-id': null + } + } + ] + } + ] + } + ] + } + ] + }) + }) + + it('Check traverse', () => { + const t1 = '**[link](foo) is bold**"' + const msg = parseMessageMarkdown(t1, 'ref://', 'http://', 'http://') + + const nodes = [] + traverseMarkupNode(msg, (node) => { + nodes.push(node) + }) + expect(nodes.length).toEqual(5) + const marks = [] + traverseAllMarks(msg, (node, mark) => { + marks.push(mark) + }) + expect(marks.length).toEqual(3) + }) + + it('check serialize variant', () => { + const node: MarkupNode = { + content: [ + { + content: [ + { + content: [ + { + content: [ + { text: 'test1 ', type: MarkupNodeType.text }, + { text: undefined, type: MarkupNodeType.hard_break }, + { + text: 'Italic', + marks: [{ type: MarkupMarkType.em, attrs: [] }], + type: MarkupNodeType.text + } + ], + type: MarkupNodeType.paragraph + } + ], + type: MarkupNodeType.list_item + }, + { + content: [ + { + content: [ + { text: 'test2 ', type: MarkupNodeType.text }, + { text: 'BOLD', marks: [{ type: MarkupMarkType.bold, attrs: [] }], type: MarkupNodeType.text } + ], + type: MarkupNodeType.paragraph + } + ], + type: MarkupNodeType.list_item + } + ], + type: MarkupNodeType.bullet_list + } + ], + type: MarkupNodeType.doc + } + const msg = serializeMessage(node, 'ref://', 'http://') + + expect(msg).toEqual('* test1 \\\n *Italic*\n* test2 **BOLD**') + }) + it('check serialize throw unsupported', () => { + const node: MarkupNode = { + content: [ + { + content: [ + { + content: [ + { + content: [ + { text: 'test1 ', type: MarkupNodeType.text }, + { text: undefined, type: MarkupNodeType.text }, + { text: undefined, type: (MarkupNodeType.text + 'qwe') as MarkupNodeType }, + { text: undefined, type: MarkupNodeType.text } + ], + type: MarkupNodeType.paragraph + } + ], + type: MarkupNodeType.list_item + } + ], + type: MarkupNodeType.bullet_list + } + ], + type: MarkupNodeType.doc + } + expect(() => serializeMessage(node, 'ref://', 'http://')).toThrowError( + 'Token type `textqwe` not supported by Markdown renderer' + ) + }) + + it('check markdown state', () => { + const st = new MarkdownState() + + st.text('qwe', true) + expect(st.out).toEqual('qwe') + }) + + it('check markdown state', () => { + const st = new MarkdownState() + + const o1 = st.quote("qwe'") + const o2 = st.quote('qwe"') + expect(o1).toEqual('"qwe\'"') + expect(o2).toEqual("'qwe\"'") + }) + + it('check horizontal rule', () => { + const node: MarkupNode = { + content: [ + { + attrs: {}, + type: MarkupNodeType.horizontal_rule + } + ], + type: MarkupNodeType.doc + } + expect(serializeMessage(node, 'ref://', 'http://')).toEqual('---') + }) + + it('check code_text', () => { + const node: MarkupNode = { + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'Link to ' + }, + { + type: MarkupNodeType.text, + text: 'https://hardware.it', + marks: [ + { + type: MarkupMarkType.em, + attrs: {} + }, + { + type: MarkupMarkType.link, + attrs: { + title: 'Some title', + href: 'https://hardware.it' + } + } + ] + } + ] + } + ] + } + expect(serializeMessage(node, 'ref://', 'http://')).toEqual( + 'Link to *[https://hardware.it](https://hardware.it "Some title")*' + ) + }) + + it('check swithc marks', () => { + const node: MarkupNode = { + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: 'Link to ', + marks: [ + { + type: MarkupMarkType.bold, + attrs: {} + } + ] + }, + { + type: MarkupNodeType.text, + text: 'https://hardware.it', + marks: [ + { + type: MarkupMarkType.em, + attrs: {} + }, + { + type: MarkupMarkType.bold, + attrs: {} + } + ] + } + ] + } + ] + } + expect(serializeMessage(node, 'ref://', 'http://')).toEqual('**Link to *https://hardware.it***') + }) +}) diff --git a/services/github/pod-github/src/markdown/extensions.ts b/services/github/pod-github/src/markdown/extensions.ts new file mode 100644 index 0000000000..3a48f27ca1 --- /dev/null +++ b/services/github/pod-github/src/markdown/extensions.ts @@ -0,0 +1,77 @@ +import { ServerKit } from '@hcengineering/text' + +import { AnyExtension, Extension, Node, mergeAttributes } from '@tiptap/core' + +export interface SubLinkOptions { + HTMLAttributes: Record + hasHulyText: (text: string) => boolean + hasHulyLink: (href: string) => boolean +} + +export const SubLink = Node.create({ + name: 'subLink', + + addOptions () { + return { + HTMLAttributes: {}, + hasHulyText: (text: string) => false, + hasHulyLink: (href: string) => false + } + }, + + group: 'block', + + content: 'inline*', + + parseHTML () { + // this plugin contains special parse rule that matches DOM element only when: + // - it has a special inner text + // - or it has a special link + // When no match, the element won't be parsed as sub node but will be processed by other extensions + return [ + { + tag: 'sub', + getAttrs: (el: HTMLElement | string) => { + if (typeof el !== 'string') { + if (this.options.hasHulyText(el.textContent ?? '')) { + return null + } + + const link = el.querySelector('a[href]') + const href = link?.getAttribute('href') ?? '' + if (link != null && this.options.hasHulyLink(href)) { + return null + } + } + // no match + return false + } + } + ] + }, + + renderHTML ({ HTMLAttributes }) { + return ['sub', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] + } +}) + +export interface GithubKitOptions { + sub?: Partial | false +} + +export const GithubKit = Extension.create({ + name: 'githubKit', + + addExtensions () { + return [ + ServerKit.configure({ + image: { + getBlobRef: async () => ({ src: '', srcset: '' }) + } + }), + ...(this.options.sub !== false ? [SubLink.configure({ ...this.options.sub })] : []) + ] + } +}) + +export const defaultExtensions: AnyExtension[] = [GithubKit.configure({})] diff --git a/services/github/pod-github/src/markdown/index.ts b/services/github/pod-github/src/markdown/index.ts new file mode 100644 index 0000000000..16a1efd153 --- /dev/null +++ b/services/github/pod-github/src/markdown/index.ts @@ -0,0 +1,83 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + MarkupNode, + jsonToMarkup, + markupToJSON, + MarkdownParser, + storeNodes, + storeMarks, + MarkdownState +} from '@hcengineering/text' +import { GithubKit } from './extensions' +import { hasHulyLink, hasHulyLinkText } from '../sync/guest' + +/** + * @public + */ +export function parseMessageMarkdown ( + message: string | undefined, + refUrl: string, + imageUrl: string, + guestUrl: string +): MarkupNode { + const extensions = [ + GithubKit.configure({ + sub: { + hasHulyText: hasHulyLinkText, + hasHulyLink: (href) => hasHulyLink(href, guestUrl) + } + }) + ] + const parser = new MarkdownParser(extensions, refUrl, imageUrl) + return parser.parse(message ?? '') +} + +/** + * @public + */ +export function serializeMessage (node: MarkupNode, refUrl: string, imageUrl: string): string { + const state = new MarkdownState(storeNodes, storeMarks, { tightLists: true, refUrl, imageUrl }) + state.renderContent(node) + return state.out +} + +/** + * @public + */ +export async function markupToMarkdown ( + markup: string, + refUrl: string = 'ref://', + imageUrl: string = 'http://localhost', + preprocessor?: (nodes: MarkupNode) => Promise +): Promise { + const json = markupToJSON(markup) + await preprocessor?.(json) + return serializeMessage(json, refUrl, imageUrl) +} + +/** + * @public + */ +export function markdownToMarkup ( + message: string, + refUrl: string = 'ref://', + imageUrl: string = 'http://localhost', + guestUrl: string = 'http://localhost/guest' +): string { + const json = parseMessageMarkdown(message, refUrl, imageUrl, guestUrl) + return jsonToMarkup(json) +} diff --git a/services/github/pod-github/src/notifications.ts b/services/github/pod-github/src/notifications.ts new file mode 100644 index 0000000000..63b924fe00 --- /dev/null +++ b/services/github/pod-github/src/notifications.ts @@ -0,0 +1,48 @@ +import { Account, Doc, Ref, TxOperations } from '@hcengineering/core' +import notification, { DocNotifyContext } from '@hcengineering/notification' +import { IntlString } from '@hcengineering/platform' +import github from '@hcengineering/github' + +export async function createNotification ( + client: TxOperations, + forDoc: Doc, + data: { user: Ref, message: IntlString, props: Record } +): Promise { + let docNotifyContext = await client.findOne(notification.class.DocNotifyContext, { attachedTo: forDoc._id }) + + if (docNotifyContext?._id === undefined) { + const docNotifyContextId = await client.createDoc(notification.class.DocNotifyContext, forDoc.space, { + attachedTo: forDoc._id, + attachedToClass: forDoc._class, + hidden: false, + user: data.user, + isPinned: false + }) + docNotifyContext = await client.findOne(notification.class.DocNotifyContext, { _id: docNotifyContextId }) + } + + // Check if we had already same notification send, and just unmark it viewed. + + const existing = await client.findOne(notification.class.CommonInboxNotification, { + user: data.user, + message: data.message, + props: data.props + }) + if (existing !== undefined) { + await client.update(docNotifyContext as DocNotifyContext, { + lastUpdateTimestamp: Date.now() + }) + await client.update(existing, { + isViewed: false + }) + } else { + await client.createDoc(notification.class.CommonInboxNotification, forDoc.space, { + user: data.user, + icon: github.icon.Github, + message: data.message, + props: data.props, + isViewed: false, + docNotifyContext: docNotifyContext?._id as Ref + }) + } +} diff --git a/services/github/pod-github/src/platform.ts b/services/github/pod-github/src/platform.ts new file mode 100644 index 0000000000..8f27a71021 --- /dev/null +++ b/services/github/pod-github/src/platform.ts @@ -0,0 +1,973 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// + +import chunter from '@hcengineering/chunter' +import core, { + Account, + BrandingMap, + Client, + ClientConnectEvent, + DocumentUpdate, + MeasureContext, + RateLimiter, + Ref, + TxOperations +} from '@hcengineering/core' +import github, { GithubAuthentication, GithubIntegration, makeQuery } from '@hcengineering/github' +import { MongoClientReference, getMongoClient } from '@hcengineering/mongo' +import { setMetadata } from '@hcengineering/platform' +import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage' +import serverToken, { generateToken } from '@hcengineering/server-token' +import tracker from '@hcengineering/tracker' +import { Installation } from '@octokit/webhooks-types' +import { Collection } from 'mongodb' +import { App, Octokit } from 'octokit' + +import { ClientWorkspaceInfo } from '@hcengineering/account' +import { Analytics } from '@hcengineering/analytics' +import { SplitLogger } from '@hcengineering/analytics-service' +import contact, { Person, PersonAccount } from '@hcengineering/contact' +import { type StorageAdapter } from '@hcengineering/server-core' +import { join } from 'path' +import { getWorkspaceInfo } from './account' +import { createPlatformClient } from './client' +import config from './config' +import { registerLoaders } from './loaders' +import { createNotification } from './notifications' +import { errorToObj } from './sync/utils' +import { GithubIntegrationRecord, GithubUserRecord } from './types' +import { GithubWorker, syncUser } from './worker' + +export interface InstallationRecord { + installationName: string + login: string + loginNodeId: string + type: 'Bot' | 'User' | 'Organization' + octokit: Octokit +} + +export class PlatformWorker { + private readonly clients: Map = new Map() + + storageAdapter!: StorageAdapter + + installations = new Map() + + integrations: GithubIntegrationRecord[] = [] + + mongoRef!: MongoClientReference + + integrationCollection!: Collection + usersCollection!: Collection + periodicTimer: any + periodicSyncPromise: Promise | undefined + + canceled = false + + private constructor ( + readonly ctx: MeasureContext, + readonly app: App, + readonly brandingMap: BrandingMap, + readonly periodicSyncInterval = 10 * 60 * 1000 // 10 minutes + ) { + setMetadata(serverToken.metadata.Secret, config.ServerSecret) + registerLoaders() + } + + public async initStorage (): Promise { + this.mongoRef = getMongoClient(config.MongoURL) + const mongoClient = await this.mongoRef.getClient() + + const db = mongoClient.db(config.ConfigurationDB) + this.integrationCollection = db.collection('installations') + this.usersCollection = db.collection('users') + + const storageConfig = storageConfigFromEnv() + this.storageAdapter = buildStorageFromConfig(storageConfig, config.MongoURL) + } + + async close (): Promise { + this.canceled = true + await Promise.all( + [...this.clients.values()].map(async (worker) => { + await worker.close() + }) + ) + this.clients.clear() + await this.storageAdapter.close() + this.mongoRef.close() + } + + async init (ctx: MeasureContext): Promise { + this.integrations = await this.integrationCollection.find({}).toArray() + await this.queryInstallations(ctx) + + const workspacesToCheck = new Set() + // We need to delete local integrations not retrieved by queryInstallations() + for (const intValue of this.integrations) { + workspacesToCheck.add(intValue.workspace) + } + for (const integr of [...this.integrations]) { + // We need to check and remove integrations without a real integration's + if (!this.installations.has(integr.installationId)) { + ctx.warn('Installation was deleted during service shutdown', { + installationId: integr.installationId, + workspace: integr.workspace + }) + this.integrations = this.integrations.filter((it) => it.installationId !== integr.installationId) + } + } + + for (const workspace of workspacesToCheck) { + // We need to connect to workspace and verify all installations and clean if required + try { + ctx.info('check clean', { workspace }) + await this.cleanWorkspaceInstallations(ctx, workspace) + } catch (err: any) { + ctx.error('failed to clean workspace', { err, workspace }) + } + } + + void this.doSyncWorkspaces().catch((err) => { + ctx.error('error during sync workspaces', { err }) + process.exit(1) + }) + + this.periodicTimer = setInterval(() => { + if (this.periodicSyncPromise === undefined) { + this.periodicSyncPromise = this.performPeriodicSync() + } + }, this.periodicSyncInterval) + } + + async performPeriodicSync (): Promise { + // Sync authorized users information details. + const workspaces = await this.findUsersWorkspaces() + for (const [workspace, users] of workspaces) { + const worker = this.clients.get(workspace) + if (worker !== undefined) { + await this.ctx.with('syncUsers', {}, async (ctx) => { + await worker.syncUserData(ctx, users) + }) + } + } + this.periodicSyncPromise = undefined + } + + triggerCheckWorkspaces = (): void => {} + + async doSyncWorkspaces (): Promise { + while (!this.canceled) { + let errors = false + try { + errors = await this.checkWorkspaces() + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error('check workspace', err) + errors = true + } + await new Promise((resolve) => { + this.triggerCheckWorkspaces = resolve + if (errors) { + setTimeout(resolve, 5000) + } + }) + } + } + + private async findUsersWorkspaces (): Promise> { + const i = this.usersCollection.find({}) + const workspaces = new Map() + while (await i.hasNext()) { + const userInfo = await i.next() + if (userInfo !== null) { + for (const ws of Object.keys(userInfo.accounts ?? {})) { + if (this.integrations.find((it) => it.workspace === ws) === undefined) { + // No workspace integration found, let's check workspace. + workspaces.set(ws, [...(workspaces.get(ws) ?? []), userInfo]) + } + } + } + } + return workspaces + } + + public async getUsers (workspace: string): Promise { + return await this.usersCollection + .find({ + [`accounts.${workspace}`]: { $exists: true } + }) + .toArray() + } + + public async getUser (login: string): Promise { + return (await this.usersCollection.find({ _id: login }).toArray()).shift() + } + + async cleanWorkspaceInstallations (ctx: MeasureContext, workspace: string, installId?: number): Promise { + // TODO: Do not remove record from $github if we failed to clean github installations inside workspace. + const token = generateToken( + config.SystemEmail, + { + name: workspace, + productId: config.ProductID + }, + { mode: 'github' } + ) + let workspaceInfo: ClientWorkspaceInfo + try { + workspaceInfo = await getWorkspaceInfo(token) + } catch (err: any) { + this.ctx.error('Workspace not found:', { workspace }) + return + } + if (workspaceInfo === undefined) { + ctx.error('No workspace found', { workspace }) + return + } + let client: Client | undefined + try { + client = await createPlatformClient(workspace, config.ProductID, 10000) + const ops = new TxOperations(client, core.account.System) + + const wsIntegerations = await client.findAll(github.class.GithubIntegration, {}) + + for (const intValue of wsIntegerations) { + if (!this.installations.has(intValue.installationId) || intValue.installationId === installId) { + await ops.remove(intValue) + } + } + } finally { + await client?.close() + } + } + + async mapInstallation ( + ctx: MeasureContext, + workspace: string, + installationId: number, + accountId: Ref + ): Promise { + if (this.integrations.find((it) => it.installationId === installationId) != null) { + // What to do with installation in different workspace? + // Let's remove it and sync to new one. + const worker = this.clients.get(workspace) as GithubWorker + await worker?.reloadRepositories(installationId) + worker?.triggerUpdate() + + this.triggerCheckWorkspaces() + return + } + const record: GithubIntegrationRecord = { + workspace, + installationId, + accountId + } + await ctx.withLog('add integration', { workspace, installationId, accountId }, async (ctx) => { + await this.integrationCollection.insertOne(record) + this.integrations.push(record) + }) + // We need to query installations to be sure we have it, in case event is delayed or not received. + await this.updateInstallation(installationId) + + const worker = this.clients.get(workspace) as GithubWorker + await worker?.reloadRepositories(installationId) + worker?.triggerUpdate() + + this.triggerCheckWorkspaces() + } + + async removeInstallation (ctx: MeasureContext, workspace: string, installationId: number): Promise { + const installation = this.installations.get(installationId) + if (installation !== undefined) { + await installation.octokit.rest.apps.deleteInstallation({ + installation_id: installationId + }) + } + // Clean workspace + await this.cleanWorkspaceInstallations(ctx, workspace, installationId) + this.triggerCheckWorkspaces() + } + + async requestGithubAccessToken (payload: { + workspace: string + code: string + state: string + accountId: Ref + }): Promise { + try { + const uri = + 'https://github.com/login/oauth/access_token?' + + makeQuery({ + client_id: config.ClientID, + client_secret: config.ClientSecret, + code: payload.code, + state: payload.state + }) + const result = await fetch(uri, { + method: 'POST', + headers: { + Accept: 'application/json' + } + }) + + const resultJson = await result.json() + if (resultJson.error !== undefined) { + await this.updateAccountAuthRecord(payload, { error: null }, undefined, false) + } else { + const okit = new Octokit({ + auth: resultJson.access_token, + client_id: config.ClientID, + client_secret: config.ClientSecret + }) + const user = await okit.rest.users.getAuthenticated() + const nowTime = Date.now() / 1000 + const dta: GithubUserRecord = { + _id: user.data.login, + token: resultJson.access_token, + code: null, + expiresIn: resultJson.expires_in != null ? nowTime + (resultJson.expires_in as number) : null, + refreshToken: resultJson.refresh_token ?? null, + refreshTokenExpiresIn: + resultJson.refresh_token_expires_in !== undefined + ? nowTime + (resultJson.refresh_token_expires_in as number) + : null, + scope: resultJson.scope, + accounts: { [payload.workspace]: payload.accountId } + } + const [existingUser] = await this.usersCollection.find({ _id: user.data.login }).toArray() + if (existingUser === undefined) { + await this.usersCollection.insertOne(dta) + } else { + dta.accounts = { ...existingUser.accounts, [payload.workspace]: payload.accountId } + await this.usersCollection.updateOne({ _id: dta._id }, { $set: dta } as any) + } + + // Update workspace client login info. + await this.updateAccountAuthRecord( + payload, + { + login: dta._id, + error: null, + avatar: user.data.avatar_url, + name: user.data.name ?? '', + url: user.data.url + }, + dta, + false + ) + } + } catch (err: any) { + Analytics.handleError(err) + await this.updateAccountAuthRecord(payload, { error: errorToObj(err) }, undefined, false) + } + } + + private async updateAccountAuthRecord ( + payload: { workspace: string, accountId: Ref }, + update: DocumentUpdate, + dta: GithubUserRecord | undefined, + revoke: boolean + ): Promise { + try { + let platformClient: Client | undefined + let shouldClose = false + try { + platformClient = this.clients.get(payload.workspace)?.client + if (platformClient === undefined) { + shouldClose = true + platformClient = await createPlatformClient(payload.workspace, config.ProductID, 30000) + } + const client = new TxOperations(platformClient, payload.accountId) + + const personAuth = await client.findOne(github.class.GithubAuthentication, { + attachedTo: payload.accountId + }) + if (personAuth !== undefined) { + if (revoke) { + await client.remove(personAuth, Date.now(), payload.accountId) + } else { + await client.update(personAuth, update, false, Date.now(), payload.accountId) + } + } + + // We need to re-bind previously created github:login account to a proper person. + const account = (await client.findOne(core.class.Account, { _id: payload.accountId })) as PersonAccount + const person = (await client.findOne(contact.class.Person, { _id: account.person })) as Person + if (person !== undefined) { + if (!revoke) { + await createNotification(client, person, { + user: account._id, + message: github.string.AuthenticatedWithGithub, + props: { + login: update.login + } + }) + + const githubAccount = (await client.findOne(core.class.Account, { + email: 'github:' + update.login + })) as PersonAccount + if (githubAccount !== undefined && githubAccount.person !== account.person) { + const dummyPerson = githubAccount.person + // To add activity entry to dummy person. + await client.update(githubAccount, { person: account.person }, false, Date.now(), payload.accountId) + + const dPerson = (await client.findOne(contact.class.Person, { _id: dummyPerson })) as Person + if (person !== undefined && dPerson !== undefined) { + await createNotification(client, dPerson, { + user: account._id, + message: github.string.AuthenticatedWithGithubEmployee, + props: { + login: update.login + } + }) + } + } + } else { + await createNotification(client, person, { + user: account._id, + message: github.string.AuthenticationRevokedGithub, + props: { + login: update.login + } + }) + } + } + + if (dta !== undefined && personAuth !== undefined) { + try { + await syncUser(this.ctx, dta, personAuth, client, payload.accountId) + } catch (err: any) { + if (err.response?.data?.message === 'Bad credentials') { + await this.revokeUserAuth(dta) + } else { + this.ctx.error(`Failed to sync user ${dta._id}`, { error: errorToObj(err) }) + } + } + } + } finally { + if (shouldClose) { + await platformClient?.close() + } + } + } catch (err: any) { + Analytics.handleError(err) + } + } + + async checkRefreshToken (auth: GithubUserRecord, force: boolean = false): Promise { + if (auth.refreshToken != null && auth.expiresIn != null && auth.expiresIn < Date.now() / 1000) { + const uri = + 'https://github.com/login/oauth/access_token?' + + makeQuery({ + client_id: config.ClientID, + client_secret: config.ClientSecret, + grant_type: 'refresh_token', + refresh_token: auth.refreshToken + }) + + const result = await fetch(uri, { + method: 'POST', + headers: { + Accept: 'application/json' + } + }) + const resultJson = await result.json() + + if (resultJson.error !== undefined) { + // We need to clear github integration info. + await this.revokeUserAuth(auth) + } else { + // Update okit + const nowTime = Date.now() / 1000 + const dta: GithubUserRecord = { + ...auth, + token: resultJson.access_token, + code: null, + expiresIn: nowTime + (resultJson.expires_in as number), + refreshToken: resultJson.refresh_token, + refreshTokenExpiresIn: nowTime + (resultJson.refresh_token_expires_in as number), + scope: resultJson.scope + } + auth.token = resultJson.access_token + auth.code = null + auth.expiresIn = dta.expiresIn + auth.refreshToken = dta.refreshToken + auth.refreshTokenExpiresIn = dta.refreshTokenExpiresIn + auth.scope = dta.scope + + await this.usersCollection.updateOne({ _id: dta._id }, { $set: dta } as any) + } + } + } + + async getAccount (login: string): Promise { + return (await this.usersCollection.findOne({ _id: login })) ?? undefined + } + + async getAccountByRef (workspace: string, ref: Ref): Promise { + return (await this.usersCollection.findOne({ [`accounts.${workspace}`]: ref })) ?? undefined + } + + private async updateInstallation (installationId: number): Promise { + const install = await this.app.octokit.rest.apps.getInstallation({ installation_id: installationId }) + if (install !== null) { + const tinst = install.data as Installation + const val: InstallationRecord = { + octokit: await this.app.getInstallationOctokit(installationId), + login: tinst.account.login, + loginNodeId: tinst.account.node_id, + type: tinst.account?.type ?? 'User', + installationName: `${tinst.account?.html_url ?? ''}` + } + this.installations.set(installationId, val) + } + } + + private async queryInstallations (ctx: MeasureContext): Promise { + for await (const install of this.app.eachInstallation.iterator()) { + const tinst = install.installation as Installation + const val: InstallationRecord = { + octokit: install.octokit, + login: tinst.account.login, + loginNodeId: tinst.account.node_id, + type: tinst.account?.type ?? 'User', + installationName: `${tinst.account?.html_url ?? ''}` + } + this.installations.set(install.installation.id, val) + ctx.info('Found installation', { + installationId: install.installation.id, + url: install.installation.account?.html_url ?? '' + }) + } + } + + async handleInstallationEvent (install: Installation, enabled: boolean): Promise { + this.ctx.info('handle integration add', { installId: install.id, name: install.html_url }) + const okit = await this.app.getInstallationOctokit(install.id) + const iName = `${install.account.html_url ?? ''}` + this.installations.set(install.id, { + octokit: okit, + login: install.account.login, + type: install.account?.type ?? 'User', + loginNodeId: install.account.node_id, + installationName: iName + }) + + const worker = this.getWorker(install.id) + if (worker !== undefined) { + await worker.syncUserData(this.ctx, await this.getUsers(worker.workspace.name)) + await worker.reloadRepositories(install.id) + + worker.triggerUpdate() + worker.triggerSync() + } + + // Need to inform workspace + const integeration = this.integrations.find((it) => it.installationId === install.id) + if (integeration !== undefined) { + const worker = this.clients.get(integeration.workspace) as GithubWorker + worker?.triggerUpdate() + } + + // Check if no workspace was available + this.triggerCheckWorkspaces() + } + + async handleInstallationEventDelete (installId: number): Promise { + const existing = this.installations.get(installId) + this.installations.delete(installId) + this.ctx.info('handle integration delete', { installId, name: existing?.installationName }) + + const interg = this.integrations.find((it) => it.installationId === installId) + + // We already have, worker we need to update it. + const worker = this.getWorker(installId) ?? (interg !== undefined ? this.clients.get(interg.workspace) : undefined) + if (worker !== undefined) { + const integeration = worker.integrations.get(installId) + if (integeration !== undefined) { + integeration.enabled = false + integeration.synchronized = new Set() + await worker._client.remove(integeration.integration) + } + worker.integrations.delete(installId) + worker.triggerUpdate() + } else { + this.ctx.info('No worker for removed installation', { installId, name: existing?.installationName }) + // No worker + } + this.integrations = this.integrations.filter((it) => it.installationId !== installId) + await this.integrationCollection.deleteOne({ installationId: installId }) + this.triggerCheckWorkspaces() + } + + async getWorkspaces (): Promise { + const workspaces = new Set(this.integrations.map((it) => it.workspace)) + + return Array.from(workspaces) + } + + private async checkWorkspaces (): Promise { + let workspaces = await this.getWorkspaces() + if (process.env.GITHUB_USE_WS !== undefined) { + workspaces = [process.env.GITHUB_USE_WS] + } + const toDelete = new Set(this.clients.keys()) + + const rateLimiter = new RateLimiter(5) + let errors = 0 + let idx = 0 + for (const workspace of workspaces) { + const widx = ++idx + if (this.clients.has(workspace)) { + toDelete.delete(workspace) + continue + } + await rateLimiter.add(async () => { + const token = generateToken( + config.SystemEmail, + { + name: workspace, + productId: config.ProductID + }, + { mode: 'github' } + ) + let workspaceInfo: ClientWorkspaceInfo | undefined + try { + workspaceInfo = await getWorkspaceInfo(token) + } catch (err: any) { + this.ctx.error('Workspace not found:', { workspace }) + errors++ + return + } + try { + if (workspaceInfo?.workspace === undefined) { + this.ctx.error('No workspace exists for workspaceId', { workspace }) + errors++ + return + } + const branding = Object.values(this.brandingMap).find((b) => b.key === workspaceInfo?.branding) ?? null + const workerCtx = this.ctx.newChild('worker', { workspace: workspaceInfo.workspace }, {}) + workerCtx.info('************************* Register worker ************************* ', { + workspaceId: workspaceInfo.workspaceId, + workspace: workspaceInfo.workspace, + index: widx, + total: workspaces.length + }) + const worker = await GithubWorker.create( + this, + workerCtx, + this.installations, + { + name: workspace, + productId: config.ProductID, + workspaceUrl: workspaceInfo.workspace, + workspaceName: workspace + }, + branding, + this.app, + this.storageAdapter, + (workspace, event) => { + if (event === ClientConnectEvent.Refresh || event === ClientConnectEvent.Upgraded) { + void this.clients.get(workspace)?.refreshClient(event === ClientConnectEvent.Upgraded) + } + } + ) + if (worker !== undefined) { + workerCtx.info('Register worker Done', { + workspaceId: workspaceInfo.workspaceId, + workspace: workspaceInfo.workspace, + index: widx, + total: workspaces.length + }) + // No if no integration, we will try connect one more time in a time period + this.clients.set(workspace, worker) + } else { + errors++ + } + } catch (e: any) { + Analytics.handleError(e) + this.ctx.info("Couldn't create WS worker", { workspace, error: e }) + console.error(e) + errors++ + } + }) + } + try { + await rateLimiter.waitProcessing() + } catch (e: any) { + Analytics.handleError(e) + errors++ + } + // Close deleted workspaces + for (const deleted of Array.from(toDelete.keys())) { + const ws = this.clients.get(deleted) + if (ws !== undefined) { + try { + await ws.ctx.logger.close() + this.ctx.info('workspace removed from tracking list', { workspace: deleted }) + this.clients.delete(deleted) + await ws.close() + } catch (err: any) { + Analytics.handleError(err) + errors++ + } + } + } + return errors > 0 + } + + getWorkers (): GithubWorker[] { + return Array.from(this.clients.values()) + } + + static async create (ctx: MeasureContext, app: App, brandingMap: BrandingMap): Promise { + const worker = new PlatformWorker(ctx, app, brandingMap) + await worker.initStorage() + await worker.init(ctx) + worker.initWebhooks() + return worker + } + + initWebhooks (): void { + const webhook = this.ctx.newChild( + 'webhook', + {}, + {}, + new SplitLogger('webhook', { root: join(process.cwd(), 'logs'), pretty: true, enableConsole: false }) + ) + webhook.info('Register webhook') + + this.app.webhooks.onAny(async (event) => { + const shortData: Record = { id: event.id, name: event.name } + if ('action' in event.payload) { + shortData.action = event.payload.action + } + this.ctx.info('webhook event', shortData) + webhook.info('event', { ...shortData, payload: event.payload }) + }) + + this.app.webhooks.on('github_app_authorization', async (event) => { + if (event.payload.action === 'revoked') { + const sender = event.payload.sender + + const records = await this.usersCollection.find({ _id: sender.login }).toArray() + for (const r of records) { + await this.revokeUserAuth(r) + } + await this.usersCollection.deleteOne({ _id: sender.login }) + } + }) + + this.app.webhooks.onError(async (event) => { + this.ctx.error('webhook event', { message: event.message, name: event.name, cause: event.cause }) + webhook.error('event', { ...event }) + }) + + function catchEventError ( + promise: Promise, + action: string, + name: string, + id: string, + repository: string + ): void { + void promise.catch((err) => { + webhook.error('error during handleEvent', { err, event: action, repository, name, id }) + Analytics.handleError(err) + }) + } + + this.app.webhooks.on('pull_request', async ({ payload, name, id }) => { + const repoWorker = this.getWorker(payload.installation?.id) + if (repoWorker !== undefined) { + catchEventError( + repoWorker.handleEvent(github.class.GithubPullRequest, payload.installation?.id, payload), + payload.action, + name, + id, + payload.repository.name + ) + } + }) + + this.app.webhooks.on('issues', async ({ payload, name, id }) => { + const repoWorker = this.getWorker(payload.installation?.id) + if (repoWorker !== undefined) { + catchEventError( + repoWorker.handleEvent(tracker.class.Issue, payload.installation?.id, payload), + payload.action, + name, + id, + payload.repository.name + ) + } + }) + this.app.webhooks.on('issue_comment', async ({ payload, name, id }) => { + const repoWorker = this.getWorker(payload.installation?.id) + if (repoWorker !== undefined) { + catchEventError( + repoWorker.handleEvent(chunter.class.ChatMessage, payload.installation?.id, payload), + payload.action, + name, + id, + payload.repository.name + ) + } + }) + + this.app.webhooks.on('repository', async ({ payload, name, id }) => { + const repoWorker = this.getWorker(payload.installation?.id) + if (repoWorker !== undefined) { + catchEventError( + repoWorker.handleEvent(github.mixin.GithubProject, payload.installation?.id, payload), + payload.action, + name, + id, + payload.repository.name + ) + } + }) + + this.app.webhooks.on('projects_v2_item', async ({ payload, name, id }) => { + const repoWorker = this.getWorker(payload.installation?.id) + if (repoWorker !== undefined) { + if (payload.projects_v2_item.content_type === 'Issue') { + catchEventError( + repoWorker.handleEvent(tracker.class.Issue, payload.installation?.id, payload), + payload.action, + name, + id, + payload.projects_v2_item.node_id + ) + } else if (payload.projects_v2_item.content_type === 'PullRequest') { + catchEventError( + repoWorker.handleEvent(github.class.GithubPullRequest, payload.installation?.id, payload), + payload.action, + name, + id, + payload.projects_v2_item.node_id + ) + } + } + }) + + this.app.webhooks.on('installation', async ({ payload, name, id }) => { + switch (payload.action) { + case 'created': + case 'unsuspend': { + catchEventError( + this.handleInstallationEvent(payload.installation, true), + payload.action, + name, + id, + payload.installation.html_url + ) + break + } + case 'suspend': { + catchEventError( + this.handleInstallationEvent(payload.installation, false), + payload.action, + name, + id, + payload.installation.html_url + ) + break + } + case 'deleted': { + catchEventError( + this.handleInstallationEventDelete(payload.installation.id), + payload.action, + name, + id, + payload.installation.html_url + ) + break + } + } + }) + this.app.webhooks.on('installation_repositories', async ({ payload, name, id }) => { + const worker = this.getWorker(payload.installation.id) + if (worker === undefined) { + this.triggerCheckWorkspaces() + return + } + catchEventError( + worker.reloadRepositories(payload.installation.id), + payload.action, + name, + id, + payload.installation.html_url + ) + const doSyncUsers = async (worker: GithubWorker): Promise => { + const users = await this.getUsers(worker.workspace.name) + await worker.syncUserData(this.ctx, users) + } + catchEventError(doSyncUsers(worker), payload.action, name, id, payload.installation.html_url) + }) + + this.app.webhooks.on('pull_request_review', async ({ payload, name, id }) => { + const repoWorker = this.getWorker(payload.installation?.id) + if (repoWorker !== undefined) { + catchEventError( + repoWorker.handleEvent(github.class.GithubReview, payload.installation?.id, payload), + payload.action, + name, + id, + payload.repository.html_url + ) + } + }) + + this.app.webhooks.on('pull_request_review_comment', async ({ payload, name, id }) => { + const repoWorker = this.getWorker(payload.installation?.id) + if (repoWorker !== undefined) { + catchEventError( + repoWorker.handleEvent(github.class.GithubReviewComment, payload.installation?.id, payload), + payload.action, + name, + id, + payload.repository.html_url + ) + } + }) + this.app.webhooks.on('pull_request_review_thread', async ({ payload, name, id }) => { + const repoWorker = this.getWorker(payload.installation?.id) + if (repoWorker !== undefined) { + catchEventError( + repoWorker.handleEvent(github.class.GithubReviewThread, payload.installation?.id, payload), + payload.action, + name, + id, + payload.repository.html_url + ) + } + }) + } + + public async revokeUserAuth (record: GithubUserRecord): Promise { + for (const [ws, acc] of Object.entries(record.accounts)) { + await this.updateAccountAuthRecord({ workspace: ws, accountId: acc }, { login: record._id }, undefined, true) + } + } + + getWorker (installationId?: number): GithubWorker | undefined { + if (installationId === undefined) { + return + } + for (const w of this.clients.values()) { + for (const i of w.integrations.values()) { + if (i.installationId === installationId) { + return w + } + } + for (const i of w.integrationsRaw.values()) { + if (i.installationId === installationId) { + return w + } + } + } + } +} diff --git a/services/github/pod-github/src/server.ts b/services/github/pod-github/src/server.ts new file mode 100644 index 0000000000..2064f49c5f --- /dev/null +++ b/services/github/pod-github/src/server.ts @@ -0,0 +1,173 @@ +import { createNodeMiddleware } from '@octokit/webhooks' +import { App } from 'octokit' + +import config from './config' +import { PlatformWorker } from './platform' + +import bp from 'body-parser' +import cors from 'cors' +import express from 'express' + +import { Analytics } from '@hcengineering/analytics' +import { Account, BrandingMap, MeasureContext, Ref } from '@hcengineering/core' +import { setMetadata } from '@hcengineering/platform' +import serverClient from '@hcengineering/server-client' +import serverCore from '@hcengineering/server-core' +import { decodeToken } from '@hcengineering/server-token' + +/** + * @public + */ +export async function start (ctx: MeasureContext, brandingMap: BrandingMap): Promise { + // Create an authenticated Octokit client authenticated as a GitHub App + ctx.info('Running Huly Github integration', { appId: config.AppID, clientID: config.ClientID }) + + setMetadata(serverCore.metadata.FrontUrl, config.FrontURL) + setMetadata(serverClient.metadata.Endpoint, config.AccountsURL) + setMetadata(serverClient.metadata.UserAgent, config.ServiceID) + + const octokitApp: App = new App({ + appId: config.AppID, + privateKey: config.PrivateKey, + webhooks: { + secret: config.WebhookSecret + } + }) + + // Optional: Get & log the authenticated app's name + const { data } = await octokitApp.octokit.request('/app') + + // Read more about custom logging: https://github.com/octokit/core.js#logging + octokitApp.octokit.log.debug(`Authenticated as '${data.name as string}'`) + + // Optional: Handle errors + octokitApp.webhooks.onError((error) => { + Analytics.handleError(error) + ctx.error('error', { error, event: error.event }) + }) + + // Launch a web server to listen for GitHub webhooks + const port = config.Port + const path = '/api/webhook' + const localWebhookUrl = `http://localhost:${port}${path}` + + // See https://github.com/octokit/webhooks.js/#createnodemiddleware for all options + const middleware = createNodeMiddleware(octokitApp.webhooks as any, { path }) + + const app = express() + + app.use(middleware as any) + app.use(cors()) + app.use(bp.json()) + app.use(bp.urlencoded({ extended: true })) + + // Initialize platform worker + let worker: PlatformWorker + try { + worker = await PlatformWorker.create(ctx, octokitApp, brandingMap) + } catch (err: any) { + Analytics.handleError(err) + ctx.error('Failed to init Service', { err }) + process.exit(1) + } + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + app.post('/api/v1/installation', async (req, res) => { + try { + const payloadData: { + installationId: number + accountId: Ref + token: string + } = req.body + + const decodedToken = decodeToken(payloadData.token) + ctx.info('/api/v1/installation', { + email: decodedToken.email, + workspaceName: decodedToken.workspace.name, + body: req.body + }) + + await ctx.withLog('map-installation', {}, async (ctx) => { + await worker.mapInstallation( + ctx, + decodedToken.workspace.name, + payloadData.installationId, + payloadData.accountId + ) + }) + res.status(200) + res.json({}) + } catch (err: any) { + Analytics.handleError(err) + res.status(401) + res.json({ error: err.message }) + } + }) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + app.post('/api/v1/auth', async (req, res) => { + try { + const payloadData: { + code: string + state: string + accountId: Ref + token: string + } = req.body + + const decodedData: { + accountId: Ref + token: string + op: string + } = JSON.parse(atob(payloadData.state)) + + const decodedToken = decodeToken(decodedData.token) + + await ctx.withLog('request-github-access-token', {}, async (ctx) => { + await worker.requestGithubAccessToken({ + workspace: decodedToken.workspace.name, + accountId: payloadData.accountId, + code: payloadData.code, + state: payloadData.state + }) + }) + res.status(200) + res.json({}) + } catch (err: any) { + Analytics.handleError(err) + res.status(401) + res.json({ error: err.message }) + } + }) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + app.post('/api/v1/installation-remove', async (req, res) => { + try { + const payloadData: { + installationId: number + token: string + } = req.body + + const decodedToken = decodeToken(payloadData.token) + ctx.info('/api/v1/installation-remove', { + email: decodedToken.email, + workspaceName: decodedToken.workspace.name, + body: req.body + }) + + await ctx.withLog('map-installation', {}, async (ctx) => { + await worker.removeInstallation(ctx, decodedToken.workspace.name, payloadData.installationId) + }) + res.status(200) + res.json({}) + } catch (err: any) { + Analytics.handleError(err) + res.status(401) + res.json({ error: err.message }) + } + }) + + app.listen(port, () => { + ctx.info(`Server is listening for events at: ${localWebhookUrl}`) + ctx.info('Press Ctrl + C to quit.') + }) +} diff --git a/services/github/pod-github/src/sync/comments.ts b/services/github/pod-github/src/sync/comments.ts new file mode 100644 index 0000000000..3bdac44099 --- /dev/null +++ b/services/github/pod-github/src/sync/comments.ts @@ -0,0 +1,557 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +import chunter, { ChatMessage } from '@hcengineering/chunter' +import { PersonAccount } from '@hcengineering/contact' +import core, { + Account, + AttachedData, + Doc, + DocumentUpdate, + MeasureContext, + Ref, + TxOperations +} from '@hcengineering/core' +import { LiveQuery } from '@hcengineering/query' +import github, { DocSyncInfo, GithubIntegrationRepository, GithubProject } from '@hcengineering/github' +import { deepEqual } from 'fast-equals' +import { + ContainerFocus, + DocSyncManager, + ExternalSyncField, + IntegrationContainer, + IntegrationManager, + githubExternalSyncVersion, + githubSyncVersion +} from '../types' +import { collectUpdate, deleteObjects, errorToObj, getSince, isGHWriteAllowed } from './utils' + +import { Analytics } from '@hcengineering/analytics' +import { IssueComment, IssueCommentCreatedEvent, IssueCommentEvent } from '@octokit/webhooks-types' +import config from '../config' +import { syncConfig } from './syncConfig' + +interface MessageData { + message: string +} + +type CommentExternalData = Omit + +export class CommentSyncManager implements DocSyncManager { + provider!: IntegrationManager + + createCommentPromise: Promise> | undefined + + externalDerivedSync = false + + constructor ( + readonly ctx: MeasureContext, + readonly client: TxOperations, + readonly lq: LiveQuery + ) {} + + async init (provider: IntegrationManager): Promise { + this.provider = provider + } + + eventSync = new Map>() + async handleEvent(integration: IntegrationContainer, derivedClient: TxOperations, evt: T): Promise { + await this.createCommentPromise + const event = evt as IssueCommentEvent + this.ctx.info('comments:handleEvent', { + action: event.action, + login: event.sender.login, + workspace: this.provider.getWorkspaceId().name + }) + + if (event.sender.type === 'Bot') { + // Ignore events from Bot if it is our bot + // No need to handle event from ourself + if (event.sender.login.includes(config.BotName)) { + return + } + } + + await this.eventSync.get(event.issue.url) + const promise = this.processEvent(event, derivedClient, integration) + this.eventSync.set(event.issue.url, promise) + await promise + this.eventSync.delete(event.issue.url) + } + + async handleDelete ( + existing: Doc | undefined, + info: DocSyncInfo, + derivedClient: TxOperations, + deleteExisting: boolean + ): Promise { + const container = await this.provider.getContainer(info.space) + if (container === undefined) { + return false + } + if ( + container?.container === undefined || + ((container.project.projectNodeId === undefined || + !container.container.projectStructure.has(container.project._id)) && + syncConfig.MainProject) + ) { + return false + } + + const commentExternal = info.external as CommentExternalData | undefined + + if (commentExternal === undefined) { + // No external issue yet, safe delete, since platform document will be deleted a well. + return true + } + const account = + existing?.createdBy ?? (await this.provider.getAccountU(commentExternal.user))?._id ?? core.account.System + + if (commentExternal !== undefined) { + try { + await this.deleteGithubDocument(container, account, commentExternal.node_id) + } catch (err: any) { + let cnt = false + if (Array.isArray(err.errors)) { + for (const e of err.errors) { + if (e.type === 'NOT_FOUND') { + // Ok issue is already deleted + cnt = true + break + } + } + } + if (!cnt) { + Analytics.handleError(err) + await derivedClient.update(info, { error: errorToObj(err) }) + } + } + } + + if (existing !== undefined && deleteExisting) { + await deleteObjects(this.ctx, this.client, [existing], account) + } + return true + } + + async deleteGithubDocument (container: ContainerFocus, account: Ref, id: string): Promise { + const okit = (await this.provider.getOctokit(account as Ref)) ?? container.container.octokit + + const q = `mutation deleteComment($commentID: ID!) { + deleteIssueComment( + input: {id: $commentID} + ) { + __typename + } + }` + if (isGHWriteAllowed()) { + await okit?.graphql(q, { + commentID: id + }) + } + } + + private async processEvent ( + event: IssueCommentEvent, + derivedClient: TxOperations, + integration: IntegrationContainer + ): Promise { + const { repository } = await this.provider.getProjectAndRepository(event.repository.node_id) + if (repository === undefined) { + this.ctx.info('No project for repository', { + repository: event.repository, + workspace: this.provider.getWorkspaceId().name + }) + return + } + + const account = (await this.provider.getAccountU(event.sender))?._id ?? core.account.System + switch (event.action) { + case 'created': { + await this.createSyncData(event, derivedClient, repository) + break + } + case 'deleted': { + const syncData = await this.client.findOne(github.class.DocSyncInfo, { + url: (event.comment.url ?? '').toLowerCase() + }) + if (syncData !== undefined) { + await derivedClient.update(syncData, { deleted: true, needSync: '' }) + this.provider.sync() + } + break + } + case 'edited': { + const commentData = await this.client.findOne(github.class.DocSyncInfo, { + url: (event.comment.url ?? '').toLowerCase() + }) + + const messageData: MessageData = { + message: await this.provider.getMarkup(integration, event.comment.body) + } + + if (commentData !== undefined) { + const chatMessage: ChatMessage | undefined = await this.client.findOne(commentData.objectClass, { + _id: commentData._id as unknown as Ref + }) + if (chatMessage !== undefined) { + const lastModified = new Date(event.comment.updated_at).getTime() + await derivedClient.diffUpdate( + commentData, + { + external: event.comment, + current: messageData, + needSync: githubSyncVersion, + lastModified + }, + lastModified + ) + await this.client.diffUpdate(chatMessage, messageData, lastModified, account) + this.provider.sync() + } + } + break + } + } + } + + private async createSyncData ( + createdEvent: IssueCommentCreatedEvent, + derivedClient: TxOperations, + repo: GithubIntegrationRepository + ): Promise { + const commentData = await this.client.findOne(github.class.DocSyncInfo, { + url: (createdEvent.comment.url ?? '').toLowerCase() + }) + + if (commentData === undefined) { + await derivedClient.createDoc(github.class.DocSyncInfo, repo.githubProject as Ref, { + url: (createdEvent.comment.url ?? '').toLowerCase(), + needSync: '', + githubNumber: 0, + repository: repo._id, + objectClass: chunter.class.ChatMessage, + external: createdEvent.comment as CommentExternalData, + externalVersion: githubExternalSyncVersion, + parent: createdEvent.issue.url, + lastModified: new Date(createdEvent.comment.updated_at).getTime() + }) + this.provider.sync() + } + } + + async sync ( + existing: Doc | undefined, + info: DocSyncInfo, + parent: DocSyncInfo | undefined, + derivedClient: TxOperations + ): Promise | undefined> { + const container = await this.provider.getContainer(info.space) + if (container?.container === undefined) { + return {} + } + if (info.external === undefined) { + // TODO: Use selected repository + const repo = container.repository.find((it) => it._id === parent?.repository) + if (repo?.nodeId === undefined) { + // No need to sync if parent repository is not defined. + return { needSync: githubSyncVersion } + } + + // If no external document, we need to create it. + this.createCommentPromise = this.createGithubComment(container, existing, info, parent, derivedClient) + return await this.createCommentPromise + } + const comment = info.external as CommentExternalData + if (parent === undefined) { + // Find parent by issue url + parent = await this.client.findOne(github.class.DocSyncInfo, { + url: (comment.html_url.split('#')?.[0] ?? '').toLowerCase() + }) + } + if (parent === undefined) { + // no Sync until parent is found, parent should trigger all child's refresh. + return { needSync: githubSyncVersion } + } + + const account = existing?.modifiedBy ?? (await this.provider.getAccountU(comment.user))?._id ?? core.account.System + + const messageData: MessageData = { + message: await this.provider.getMarkup(container.container, comment.body) + } + if (existing === undefined) { + try { + await this.createComment(info, messageData, parent, comment, account) + return { needSync: githubSyncVersion, current: messageData } + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error(err) + return { needSync: githubSyncVersion, error: errorToObj(err) } + } + } else { + await this.handleDiffUpdate(existing, info, messageData, container, parent, comment, account) + } + return { current: messageData, needSync: githubSyncVersion } + } + + private async handleDiffUpdate ( + existing: Doc, + info: DocSyncInfo, + messageData: MessageData, + container: ContainerFocus, + parent: DocSyncInfo, + comment: CommentExternalData, + account: Ref + ): Promise { + const repository = container.repository.find((it) => it._id === info.repository) + if (repository === undefined) { + return + } + + const existingComment = existing as ChatMessage + + const previousData: MessageData = info.current ?? ({} as unknown as MessageData) + + const update = collectUpdate(previousData, messageData, Object.keys(messageData)) + + const platformUpdate = collectUpdate(previousData, existing, Object.keys(messageData)) + + // We should remove changes we already have from github changed. + for (const [k, v] of Object.entries(update)) { + if ((platformUpdate as any)[k] !== v) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (platformUpdate as any)[k] + } + } + // Remove current same values from update + for (const [k, v] of Object.entries(existingComment)) { + if ((update as any)[k] === v) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (update as any)[k] + } + } + + if (Object.keys(platformUpdate).length > 0) { + // Check and update body with external + const okit = + (await this.provider.getOctokit(existing.modifiedBy as Ref)) ?? container.container.octokit + await okit?.rest.issues.updateComment({ + owner: repository.owner?.login as string, + repo: repository.name, + issue_number: parent.githubNumber, + comment_id: comment.id, + body: await this.provider.getMarkdown(existingComment.message), + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }) + } + if (Object.keys(update).length > 0) { + await this.client.update(existing, update, false, new Date(comment.updated_at).getTime(), account) + } + } + + private async createComment ( + info: DocSyncInfo, + messageData: MessageData, + parent: DocSyncInfo, + comment: CommentExternalData, + account: Ref + ): Promise { + const _id: Ref = info._id as unknown as Ref + const value: AttachedData = { + ...messageData, + attachments: 0 + } + await this.client.addCollection( + chunter.class.ChatMessage, + info.space, + parent._id, + parent.objectClass, + 'comments', + value, + _id, + new Date(comment.created_at).getTime(), + account + ) + } + + async createGithubComment ( + container: ContainerFocus, + existing: Doc | undefined, + info: DocSyncInfo, + parent: DocSyncInfo | undefined, + derivedClient: TxOperations + ): Promise> { + // TODO: Use selected repository + const repo = container.repository.find((it) => it._id === parent?.repository) + if (repo?.nodeId === undefined) { + // No need to sync if parent repository is not defined. + return { needSync: githubSyncVersion } + } + + if (parent === undefined) { + return {} + } + const chatMessage = existing as ChatMessage + const okit = + (await this.provider.getOctokit(chatMessage.modifiedBy as Ref)) ?? container.container.octokit + + // No external version yet, create it. + try { + const result = await okit?.rest.issues.createComment({ + owner: repo.owner?.login as string, + repo: repo.name, + issue_number: parent.githubNumber, + body: await this.provider.getMarkdown(chatMessage.message), + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }) + const upd: DocumentUpdate = { + parent: result?.data.html_url?.split('#')?.[0] ?? '', + url: (result?.data.url ?? '').toLowerCase(), + external: result?.data as CommentExternalData, + current: result?.data, + repository: repo._id + } + // We need to update in current promise, to prevent event changes. + await derivedClient.update(info, upd) + return {} + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error(err) + return { needSync: githubSyncVersion, error: errorToObj(err) } + } + } + + async externalSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + kind: ExternalSyncField, + syncDocs: DocSyncInfo[], + repository: GithubIntegrationRepository, + project: GithubProject + ): Promise { + // No need to perform external sync for comments, so let's update marks + const tx = derivedClient.apply('comments_github') + for (const d of syncDocs) { + await tx.update(d, { externalVersion: githubExternalSyncVersion }) + } + await tx.commit() + this.provider.sync() + } + + repositoryDisabled (integration: IntegrationContainer, repo: GithubIntegrationRepository): void { + integration.synchronized.delete(`${repo._id}:comment`) + } + + async externalFullSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + projects: GithubProject[], + repositories: GithubIntegrationRepository[] + ): Promise { + for (const repo of repositories) { + const syncKey = `${repo._id}:comment` + if (repo.githubProject === undefined || !repo.enabled || integration.synchronized.has(syncKey)) { + if (!repo.enabled) { + integration.synchronized.delete(syncKey) + } + continue + } + const prj = projects.find((it) => repo.githubProject === it._id) + if (prj === undefined) { + continue + } + + // Wait global project sync + await integration.syncLock.get(prj._id) + + const since = await getSince(this.client, chunter.class.ChatMessage, repo) + + const i = integration.octokit.paginate.iterator(integration.octokit.rest.issues.listCommentsForRepo, { + owner: repo.owner?.login as string, + repo: repo.name, + state: 'all', + sort: 'updated', + direction: 'asc', + since, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }) + try { + for await (const data of i) { + const comments: CommentExternalData[] = data.data as any + this.ctx.info('retrieve comments for', { + repo: repo.name, + comments: comments.length, + used: data.headers['x-ratelimit-used'], + limit: data.headers['x-ratelimit-limit'], + workspace: this.provider.getWorkspaceId().name + }) + await this.syncComments(repo, comments, derivedClient) + this.provider.sync() + } + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error(err) + } + integration.synchronized.add(syncKey) + } + } + + async syncComments ( + repo: GithubIntegrationRepository, + comments: CommentExternalData[], + derivedClient: TxOperations + ): Promise { + if (repo.githubProject == null) { + return + } + const syncInfo = await this.client.findAll(github.class.DocSyncInfo, { + space: repo.githubProject, + repository: repo._id, + objectClass: chunter.class.ChatMessage, + url: { $in: comments.map((it) => (it.url ?? '').toLowerCase()) } + }) + + for (const comment of comments) { + try { + const existing = syncInfo.find((it) => it.url === comment.url.toLowerCase()) + const lastModified = new Date(comment.updated_at).getTime() + if (existing === undefined) { + await derivedClient.createDoc(github.class.DocSyncInfo, repo.githubProject, { + url: comment.url.toLowerCase(), + needSync: '', + githubNumber: 0, + objectClass: chunter.class.ChatMessage, + external: comment, + externalVersion: githubExternalSyncVersion, + parent: comment.html_url.split('#')?.[0], + repository: repo._id, + lastModified + }) + } else { + if (!deepEqual(existing.external, comment) || existing.externalVersion !== githubExternalSyncVersion) { + await derivedClient.diffUpdate( + existing, + { + needSync: '', + external: comment, + externalVersion: githubExternalSyncVersion, + lastModified + }, + lastModified + ) + this.provider.sync() + } + } + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error(err) + } + } + } +} diff --git a/services/github/pod-github/src/sync/githubTypes.ts b/services/github/pod-github/src/sync/githubTypes.ts new file mode 100644 index 0000000000..e72395795e --- /dev/null +++ b/services/github/pod-github/src/sync/githubTypes.ts @@ -0,0 +1,873 @@ +import { + GithubIssueStateReason, + GithubPullRequestReviewState, + GithubPullRequestState, + GithubReviewDecisionState, + PullRequestMergeable +} from '@hcengineering/github' + +/** + * @public + */ +export type GithubDataType = 'SINGLE_SELECT' | 'TEXT' | 'DATE' | 'NUMBER' + +/** + * @public + */ +export interface GithubProjectV2 { + projectV2: { + id: string + title: string + description: string + updatedAt: string + fields: { + edges: GithubProjectV2Field[] + } + } +} + +/** + * @public + */ +export interface GithubProjectV2FieldOption { + name: string + color: string + description: string + id: string +} +/** + * @public + */ +export interface GithubProjectV2Field { + node: { + dataType: GithubDataType + updatedAt: string + + id: string + name: string + options?: GithubProjectV2FieldOption[] + } & Record +} +/** + * @public + */ +export interface GithubProjectV2ItemFieldValue { + id: string + // Date + date?: string + // Number + number?: number + // Single select + color?: string + description?: string + optionId?: string + // Text + text?: string + field: { + id: string + name: string + dataType: GithubDataType + } +} + +export interface GithubProjectV2Item { + id: string + type: 'ISSUE' | 'PULL_REQUEST' | 'DRAFT_ISSUE' | 'REDACTED' + project: { + id: string + number: number + } + fieldValues: { + nodes: (GithubProjectV2ItemFieldValue | any)[] + } +} + +export const projectV2Field = ` + ... on ProjectV2Field { + id + name + updatedAt + dataType + } + ... on ProjectV2IterationField { + id + name + dataType + updatedAt + } + ... on ProjectV2SingleSelectField { + id + name + options { + name + id + color + description + } + dataType + updatedAt + } +` + +export const projectV2ItemFields = ` + ... on ProjectV2ItemFieldDateValue { + id + date + field { + ... on ProjectV2Field { + id + name + dataType + } + } + } + ... on ProjectV2ItemFieldNumberValue { + id + number + field { + ... on ProjectV2Field { + id + name + dataType + } + } + } + ... on ProjectV2ItemFieldSingleSelectValue { + id + name + color + description + optionId + field { + ... on ProjectV2SingleSelectField { + id + name + dataType + } + } + } + ... on ProjectV2ItemFieldTextValue { + id + text + field { + ... on ProjectV2Field { + id + name + dataType + } + } + } +` + +export const assigneesField = ` + assignees(first: 10) { + nodes { + id + login + name + email + avatarUrl + } + } +` +export const authorField = ` + author { + login + ... on User { + id + email + name + } + avatarUrl + } +` +export const labelsField = ` + labels(first: 50) { + nodes { + id + name + color + description + } + } +` +export const participantsField = ` + participants(first: 50) { + nodes { + id + login + } + } +` +export const reactionsField = ` + reactions(first: 50) { + nodes { + content + createdAt + id + user { + login + } + } + } +` + +/** + * @public + */ +export interface UserInfo { + id: string + login: string + name: string + email?: string + avatarUrl?: string +} + +/** + * @public + */ +export const issueDetails = (stateReason: boolean): string => ` +body +closed +closedAt +${authorField} +${assigneesField} +createdAt +createdViaEmail +id +${labelsField} +locked +number +${participantsField} +state +${stateReason ? 'stateReason' : ''} +title +updatedAt +url +${reactionsField} +projectItems(first: 10, includeArchived: true) { + nodes { + id + type + project { + id + url + number + } + fieldValues(first: 50) { + nodes { + ${projectV2ItemFields} + } + } + } +} +lastEditedAt +publishedAt +` + +/** + * @public + */ +export interface IssueExternalData { + closed: boolean + closedAt?: string // Date UTCZ + author: UserInfo + assignees: { + nodes: UserInfo[] + } + createdAt: string + body: string + createdViaEmail?: boolean + id: string + labels: { + nodes: { + id: string + name: string + color: string + description: string + }[] + } + locked: boolean + number: number + participants: { + nodes: UserInfo[] + } + state: 'CLOSED' | 'OPEN' | 'MERGED' + stateReason?: GithubIssueStateReason | null + title: string + updatedAt: string + url: string + reactions: { + nodes: { + content: string + createdAt: string + id: string + user: { + login: string + } + }[] + } + projectItems: { + nodes: GithubProjectV2Item[] + } + lastEditedAt: string + publishedAt: string +} + +/** + * @public + */ +export interface GithubCommit { + additions: number + authoredDate: string + authoredByCommitter: boolean + changedFiles: number + commitUrl: string + deletions: number + id: string + message: string + messageBody: string + oid: string + pushedDate: string | null + signature: { + email?: string + state: string + } + url: string + committedDate: string | null + status: { + state: CommitStatus + id: string + } +} +/** + * @public + */ +export enum GithubPatchStatus { + ADDED = 'ADDED', + // The file was added. Git status 'A'. + DELETED = 'DELETED', + // The file was deleted. Git status 'D'. + RENAMED = 'RENAMED', + // The file was renamed. Git status 'R'. + COPIED = 'COPIED', + // The file was copied. Git status 'C'. + MODIFIED = 'MODIFIED', + // The file's contents were changed. Git status 'M'. + CHANGED = 'CHANGED' + // The file's type was changed. Git status 'T'. +} +/** + * @public + */ +export enum CommitStatus { + EXPECTED = 'EXPECTED', + // Status is expected. + ERROR = 'ERROR', + // Status is errored. + FAILURE = 'FAILURE', + // Status is failing. + PENDING = 'PENDING', + // Status is pending. + SUCCESS = 'SUCCESS' + // Status is successful. +} + +export type PullRequestReviewState = 'PENDING' | 'COMMENTED' | 'APPROVED' | 'CHANGES_REQUESTED' | 'DISMISSED' + +export type AuthorAssociationType = + | 'COLLABORATOR' + | 'CONTRIBUTOR' + | 'FIRST_TIMER' + | 'FIRST_TIME_CONTRIBUTOR' + | 'MANNEQUIN' + | 'MEMBER' + | 'NONE' + | 'OWNER' + +export type MinimizeReason = 'abuse' | 'off-topic' | 'outdated' | 'resolved' | 'duplicate' | 'spam' + +export interface Review { + id: string + url: string + + state: PullRequestReviewState + author: UserInfo + + body: string + + createdAt: string + updatedAt: string | null + publishedAt: string | null + lastEditedAt: string | null + submittedAt: string | null + + isMinimized: boolean | null + minimizedReason: MinimizeReason + + authorAssociation: AuthorAssociationType + + comments: { + totalCount: number + nodes: { + url: string + }[] + } +} + +export interface ReviewThread { + id: string + line: number + subjectType: 'LINE' | 'FILE' + startLine: number + isOutdated: boolean + isResolved: boolean + diffSide: 'LEFT' | 'RIGHT' + isCollapsed: boolean + originalLine: number + originalStartLine: number | null + path: string + startDiffSide: 'LEFT' | 'RIGHT' | null + resolvedBy: UserInfo | null + comments: { + totalCount: number + nodes: ReviewComment[] + } +} +export interface ReviewComment { + id: string + url: string + body: string + createdAt: string + updatedAt: string | null + publishedAt: string | null + draftedAt: string | null + lastEditedAt: string | null + outdated: boolean + includesCreatedEdit: boolean + isMinimized: boolean + minimizedReason: MinimizeReason + line: number | null + startLine: number | null + originalLine: number | null + originalStartLine: number | null + diffHunk: string | null + path: string + replyTo: { + url: string + } | null + author: UserInfo + + pullRequestReview: { + url: string + state: PullRequestReviewState + author: UserInfo + } +} +/** + * @public + */ +export interface PullRequestExternalData extends IssueExternalData { + isDraft: boolean + additions: number + deletions: number + changedFiles: number + commits: { + nodes: { + commit: GithubCommit + }[] + } + headRefName: string + headRefOid: string + + merged: boolean + mergedAt?: string | null + mergeable: PullRequestMergeable + mergedBy?: UserInfo + state: 'OPEN' | 'CLOSED' | 'MERGED' + + reviewDecision: 'CHANGES_REQUESTED' | 'APPROVED' | 'REVIEW_REQUIRED' + headRef: { + name: string + id: string + prefix: string + } + baseRef: { + name: string + id: string + prefix: string + } + reviews: { + totalCount: number + nodes: Review[] + } + reviewThreads: { + totalCount: number + nodes: ReviewThread[] + } + + latestReviews: { + totalCount: number + nodes: Review[] + } + reviewRequests: { + totalCount: number + nodes: { + requestedReviewer: UserInfo + }[] + } + files: { + totalCount: number + nodes: { + additions: number + changeType: GithubPatchStatus + deletions: number + path: string + }[] + } +} + +/** + * @public + */ +export const pullRequestCommits = ` +commits(first: 50) { + nodes { + commit { + additions + authoredDate + authoredByCommitter + changedFiles + commitUrl + deletions + id + message + messageBody + oid + pushedDate + signature { + email + state + } + url + committedDate + status { + state + id + } + } + } +}` + +export const reviewDetailsNoComments = ` + state + author { + login + url + ... on User { + id + email + avatarUrl + login + name + } + } + url + body + createdAt + updatedAt + id + isMinimized + minimizedReason + authorAssociation + lastEditedAt + publishedAt + resourcePath + submittedAt` + +export const reviewDetails = ` + ${reviewDetailsNoComments} + comments(first: 50) { + nodes { + url + } + } +` + +export const reviewsDescr = ` +reviews(first: 50, states:[PENDING, COMMENTED, APPROVED, CHANGES_REQUESTED, DISMISSED]) { + totalCount + nodes { + ${reviewDetails} + } +} +` + +export const reviewCommentDetails = ` +id +url +body +createdAt +updatedAt +publishedAt +draftedAt +outdated +lastEditedAt +includesCreatedEdit +isMinimized +minimizedReason +line +startLine +originalLine +originalStartLine +diffHunk +path +pullRequestReview { + url + state + author { + login + url + ... on User { + id + email + avatarUrl + login + name + } + } +} +replyTo { + url +} +author { + avatarUrl + login + resourcePath + url +} +` + +export const reviewThreadDetails = ` +id +subjectType +line +startLine +isOutdated +isResolved +diffSide +isCollapsed +originalLine +originalStartLine +path +startDiffSide +resolvedBy { + url + login + id + name + email + avatarUrl +} +comments(first: 50) { + totalCount + nodes { + ${reviewCommentDetails} + } +} +` + +export const reviewRequestsDescr = ` +reviewThreads(first: 90) { + totalCount + nodes { + __typename + ${reviewThreadDetails} + } +} +` + +/** + * @public + */ +export const pullRequestDetails = ` +${issueDetails(false)} +isDraft +additions +deletions +changedFiles +${pullRequestCommits} +headRefName +headRefOid +merged +mergedAt +mergeable +state +reviewDecision +headRef { + name + id + prefix +} +baseRef { + name + id + prefix +} +mergedBy { + login + url + ... on User { + id + email + avatarUrl + login + name + } +} +${reviewsDescr} +${reviewRequestsDescr} +latestReviews(first: 50) { + totalCount + nodes { + ${reviewDetailsNoComments} + } +} +reviewRequests(first: 50) { + totalCount + nodes { + requestedReviewer { + __typename + ... on User { + login + avatarUrl + name + email + } + } + } +} +files(first: 100) { + totalCount + nodes { + additions + changeType + deletions + path + } +} +` + +/** + * @public + */ +export const projectValue = `project { + id + url + number +}` + +/** + * @public + */ +export const fieldValues = `fieldValues(first: 50) { + nodes { + ... on ProjectV2ItemFieldDateValue { + id + date + field { + ... on ProjectV2Field { + id + name + dataType + } + } + } + ... on ProjectV2ItemFieldNumberValue { + id + number + field { + ... on ProjectV2Field { + id + name + dataType + } + } + } + ... on ProjectV2ItemFieldSingleSelectValue { + id + name + color + description + optionId + field { + ... on ProjectV2SingleSelectField { + id + name + dataType + } + } + } + ... on ProjectV2ItemFieldTextValue { + id + text + field { + ... on ProjectV2Field { + id + name + dataType + } + } + } + } +}` + +/** + * @public + */ +export const supportedGithubTypes = new Set(['TEXT', 'NUMBER', 'DATA', 'SINGLE_SELECT']) + +export function toPRState (state: PullRequestExternalData['state']): GithubPullRequestState { + switch (state) { + case 'OPEN': + return GithubPullRequestState.open + case 'CLOSED': + return GithubPullRequestState.closed + case 'MERGED': + return GithubPullRequestState.merged + } +} +export function toReviewState (state: PullRequestReviewState): GithubPullRequestReviewState { + switch (state) { + case 'PENDING': + return GithubPullRequestReviewState.Pending + case 'COMMENTED': + return GithubPullRequestReviewState.Commented + case 'APPROVED': + return GithubPullRequestReviewState.Approved + case 'CHANGES_REQUESTED': + return GithubPullRequestReviewState.ChangesRequested + case 'DISMISSED': + return GithubPullRequestReviewState.Dismissed + } +} +export function toReviewDecision (reviewDecision: PullRequestExternalData['reviewDecision']): GithubReviewDecisionState { + switch (reviewDecision) { + case 'APPROVED': + return GithubReviewDecisionState.Approved + case 'REVIEW_REQUIRED': + return GithubReviewDecisionState.ReviewRequired + case 'CHANGES_REQUESTED': + return GithubReviewDecisionState.ChangesRequested + } +} + +export function getUpdatedAtReviewThread (review: ReviewThread): number { + const value = (review.comments.nodes.map((it) => it.updatedAt).filter((it) => it != null) as string[]) + .map((it) => new Date(it).getTime()) + .reduce((prev, it) => (it > prev ? it : prev), 0) + if (value === 0) { + return Date.now() + } + return value +} diff --git a/services/github/pod-github/src/sync/guest.ts b/services/github/pod-github/src/sync/guest.ts new file mode 100644 index 0000000000..39cdfe390d --- /dev/null +++ b/services/github/pod-github/src/sync/guest.ts @@ -0,0 +1,78 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +import { Branding, TxOperations, WorkspaceIdWithUrl } from '@hcengineering/core' +import { MarkupMarkType, MarkupNode, MarkupNodeType, traverseMarkupNode } from '@hcengineering/text' +import { getPublicLink } from '@hcengineering/server-guest-resources' +import { Issue } from '@hcengineering/tracker' + +const githubLinkText = process.env.LINK_TEXT ?? 'Huly®:' + +const githubLinkTextOld = 'View in Huly' + +export function hasHulyLinkText (text: string): boolean { + return text.includes(githubLinkText) || text.includes(githubLinkTextOld) +} + +export function hasHulyLink (href: string, guestLink: string): boolean { + return href.includes(guestLink) +} + +export async function stripGuestLink (markdown: MarkupNode): Promise { + const toRemove: MarkupNode[] = [] + + traverseMarkupNode(markdown, (node) => { + if (node.content === undefined) { + return + } + const oldLength = node.content.length + node.content = node.content.filter((it) => it.type !== MarkupNodeType.subLink) + + // sub is an inline node hence tiptap wraps it with a paragraph + // so we need to remove the parent paragraph node if it is empty + if (node.content.length !== oldLength && node.type === MarkupNodeType.paragraph) { + toRemove.push(node) + } + }) + + // traverse nodes once again and remove empty parent node + traverseMarkupNode(markdown, (node) => { + if (node.content === undefined) { + return + } + node.content = node.content.filter((it) => !toRemove.includes(it)) + }) +} +export async function appendGuestLink ( + client: TxOperations, + doc: Issue, + markdown: MarkupNode, + workspace: WorkspaceIdWithUrl, + branding: Branding | null +): Promise { + const publicLink = await getPublicLink(doc, client, workspace, false, branding) + await stripGuestLink(markdown) + appendGuestLinkToModel(markdown, publicLink, doc.identifier) +} + +export function appendGuestLinkToModel (markdown: MarkupNode, publicLink: string, identifier: string): void { + markdown.content = [ + ...(markdown.content ?? []), + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.subLink, + content: [ + { + type: MarkupNodeType.text, + text: githubLinkText.trim() + ' ' + identifier + '', + marks: [{ type: MarkupMarkType.link, attrs: { href: publicLink, _target: '_blank' } }] + } + ] + } + ] + } + ] +} diff --git a/services/github/pod-github/src/sync/issueBase.ts b/services/github/pod-github/src/sync/issueBase.ts new file mode 100644 index 0000000000..4b13768cfd --- /dev/null +++ b/services/github/pod-github/src/sync/issueBase.ts @@ -0,0 +1,1275 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +/* + TODO: + * Add since to synchronization +*/ +import activity from '@hcengineering/activity' +import { Analytics } from '@hcengineering/analytics' +import { CollaboratorClient } from '@hcengineering/collaborator-client' +import { PersonAccount } from '@hcengineering/contact' +import core, { + Account, + AttachedDoc, + Class, + Doc, + DocumentUpdate, + MeasureContext, + Ref, + Space, + Status, + TxOperations, + generateId, + getCollaborativeDoc, + getCollaborativeDocId +} from '@hcengineering/core' +import { IntlString } from '@hcengineering/platform' +import { LiveQuery } from '@hcengineering/query' +import task, { TaskType } from '@hcengineering/task' +import { MarkupNode, MarkupNodeType, areEqualMarkups, markupToJSON, traverseNode } from '@hcengineering/text' +import tracker, { Issue, IssuePriority } from '@hcengineering/tracker' +import time, { type ToDo } from '@hcengineering/time' +import { ProjectsV2ItemEvent } from '@octokit/webhooks-types' +import github, { + DocSyncInfo, + GithubFieldMapping, + GithubIntegrationRepository, + GithubIssue, + GithubIssue as GithubIssueP, + GithubMilestone, + GithubProject +} from '@hcengineering/github' +import { deepEqual } from 'fast-equals' +import { Octokit } from 'octokit' +import { + ContainerFocus, + IntegrationContainer, + IntegrationManager, + githubExternalSyncVersion, + githubSyncVersion +} from '../types' +import { + GithubDataType, + GithubProjectV2FieldOption, + GithubProjectV2Item, + GithubProjectV2ItemFieldValue, + IssueExternalData, + fieldValues, + projectValue, + supportedGithubTypes +} from './githubTypes' +import { appendGuestLink, stripGuestLink } from './guest' +import { syncConfig } from './syncConfig' +import { + collectUpdate, + compareMarkdown, + deleteObjects, + errorToObj, + getCreateStatus, + getType, + isGHWriteAllowed +} from './utils' + +/** + * @public + */ +export type GithubIssueData = Omit< +Issue, +| 'commits' +| 'attachments' +| 'commits' +| 'number' +| 'files' +| 'space' +| 'identifier' +| 'rank' +| 'status' +| 'priority' +| 'subIssues' +| 'parents' +| 'estimation' +| 'reportedTime' +| 'reports' +| 'childInfo' +| 'dueDate' +| 'kind' +| 'reviews' +| 'reviewThreads' +| 'reviewComments' +| 'component' +| keyof AttachedDoc +> & +Record + +/** + * @public + */ +export interface IssueSyncTarget { + project: GithubProject + mappings: GithubFieldMapping[] + target: GithubProject | GithubMilestone + prjData?: GithubProjectV2Item +} + +export abstract class IssueSyncManagerBase { + provider!: IntegrationManager + constructor ( + readonly ctx: MeasureContext, + readonly client: TxOperations, + readonly lq: LiveQuery, + readonly collaborator: CollaboratorClient + ) {} + + async init (provider: IntegrationManager): Promise { + this.provider = provider + } + + async getAssignees (issue: IssueExternalData): Promise { + // Find Assignees and reviewers + const assignees: PersonAccount[] = [] + + for (const o of issue.assignees.nodes) { + const acc = await this.provider.getAccount(o) + if (acc !== undefined) { + assignees.push(acc) + } + } + return assignees + } + + async processProjectV2Event ( + integration: IntegrationContainer, + event: ProjectsV2ItemEvent, + derivedClient: TxOperations, + prj: GithubProject + ): Promise { + const account = (await this.provider.getAccountU(event.sender))?._id ?? core.account.System + switch (event.action) { + case 'edited': { + const itemId = event.projects_v2_item.node_id + const projectId = event.projects_v2_item.project_node_id + + try { + const actualContent: { + node: { + id: string + content: { + id: string + url: string + number: number + } + fieldValues: { + nodes: GithubProjectV2ItemFieldValue[] + } + } + } = (await integration.octokit?.graphql( + `query listIssue($nodeId: ID!) { + node(id: $nodeId) { + ... on ProjectV2Item { + id + content { + ... on Issue { + id + number + url + } + ... on PullRequest { + id + number + url + } + } + ${fieldValues} + } + } + }`, + { + nodeId: itemId + } + )) as any + const syncData = await this.client.findOne(github.class.DocSyncInfo, { + url: (actualContent.node.content.url ?? '').toLowerCase() + }) + + if (syncData !== undefined) { + const milestone = ( + await this.provider.liveQuery.queryFind(github.mixin.GithubMilestone, {}) + ).find((it) => it.projectNodeId === projectId) + + const target: IssueSyncTarget | undefined = + milestone !== undefined + ? { + mappings: milestone.mappings, + project: prj, + target: milestone + } + : prj.projectNodeId === projectId + ? this.getProjectIssueTarget(prj) + : undefined + + if (target === undefined) { + // Not our project, we should just update external + return + } + + this.ctx.info('event for issue', { url: syncData.url, workspace: this.provider.getWorkspaceId().name }) + const externalData = syncData.external as IssueExternalData + // We need to replace field values we retrieved + target.prjData = externalData.projectItems.nodes.find( + (it) => it.project.id === event.projects_v2_item.project_node_id + ) + + if (target.prjData === undefined) { + target.prjData = { + fieldValues: actualContent.node.fieldValues, + id: event.projects_v2_item.node_id, + type: 'ISSUE', + project: { + id: prj.projectNodeId as string, + number: prj.projectNumber as number + } + } + externalData.projectItems.nodes.push(target.prjData) + } else { + target.prjData.fieldValues = actualContent.node.fieldValues + } + // Store github values + await derivedClient.update(syncData, { + external: externalData, + externalVersion: githubExternalSyncVersion + }) + + if (event.changes.field_value === undefined) { + this.ctx.info('No changes for change event', { event, workspace: this.provider.getWorkspaceId().name }) + return + } + + let needProjectRefresh = false + const update: DocumentUpdate & Record = {} + + let structure = integration.projectStructure.get(target.target._id) + + const repositories = await this.provider.liveQuery.queryFind( + github.class.GithubIntegrationRepository, + {} + ) + + for (const f of target.prjData.fieldValues?.nodes ?? []) { + if (!('id' in f)) { + continue + } + // Check if we need to update project structure + if (structure !== undefined) { + const ff = structure.projectV2.fields.edges.find((it) => it.node.id === f.field.id) + if (ff === undefined && supportedGithubTypes.has(f.field.dataType)) { + // We have missing field. + needProjectRefresh = true + } + } + if (needProjectRefresh) { + const repo = repositories.find((it) => it._id === syncData.repository) + + if (repo !== undefined) { + await this.provider.handleEvent(github.class.GithubIntegration, integration.installationId, repo, {}) + structure = integration.projectStructure.get(prj._id) + } + } + + const taskTypes = ( + await this.provider.liveQuery.queryFind(task.class.TaskType, { parent: prj.type }) + ).filter((it) => this.client.getHierarchy().isDerived(it.targetClass, syncData.objectClass)) + + // TODO: Use GithubProject configuration to specify target type for issues + if (taskTypes.length === 0) { + // Missing required task type + this.ctx.error('Missing required task type, in Event.') + } + + if (event.changes.field_value.field_node_id === f.field.id && taskTypes.length > 0) { + const ff = await this.toPlatformField( + { + container: integration, + project: prj, + repository: repositories.filter((it) => it.githubProject === prj._id) + }, + f, + target, + taskTypes[0] + ) + if (ff === undefined) { + continue + } + const { value, mapping } = ff + if (value !== undefined) { + update[mapping.name] = value + } + continue + } + } + if (Object.keys(update).length > 0) { + await this.handleUpdate(externalData, derivedClient, update, account, prj, false, syncData) + } + } + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error(err, event) + } + + break + } + } + } + + async handleUpdate ( + external: IssueExternalData, + derivedClient: TxOperations, + update: DocumentUpdate, + account: Ref, + prj: GithubProject, + needSync: boolean, + syncData?: DocSyncInfo, + verifyUpdate?: ( + state: DocSyncInfo, + existing: Issue, + external: IssueExternalData, + update: DocumentUpdate + ) => Promise, + extraSyncUpdate?: DocumentUpdate + ): Promise { + if (Object.keys(update).length === 0 && !needSync) { + return + } + + syncData = + syncData ?? (await this.client.findOne(github.class.DocSyncInfo, { url: (external.url ?? '').toLowerCase() })) + + if (syncData !== undefined) { + const doc: Issue | undefined = await this.client.findOne(syncData.objectClass, { + _id: syncData._id as unknown as Ref + }) + + // Use now as modified date for events. + const lastModified = new Date().getTime() + + if (doc !== undefined && ((await verifyUpdate?.(syncData, doc, external, update)) ?? true)) { + if ( + update.description !== undefined && + !areEqualMarkups(update.description, syncData.current?.description ?? '') + ) { + try { + const collaborativeDoc = getCollaborativeDoc(getCollaborativeDocId(doc._id, 'description')) + await this.collaborator.updateContent(collaborativeDoc, 'description', update.description) + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error(err) + } + } else { + delete update.description + } + + // Sync all possibly updated action items states + if (update.description !== undefined) { + const node = markupToJSON(update.description) + const todos: Record = {} + traverseNode(node, (n) => { + if (n.type === MarkupNodeType.todoItem && n.attrs?.todoid !== undefined) { + todos[n.attrs.todoid as string] = (n.attrs.checked as boolean) ?? false + } + + return true + }) + + const updateTodos = this.client.apply('todos') + for (const [k, v] of Object.entries(todos)) { + await updateTodos.updateDoc(time.class.ToDo, time.space.ToDos, k as Ref, { + doneOn: v ? Date.now() : null + }) + } + + await updateTodos.commit() + } + + await derivedClient.diffUpdate( + syncData, + { + external, + externalVersion: githubExternalSyncVersion, + current: { ...syncData.current, ...update }, + needSync: needSync ? '' : githubSyncVersion, // No need to sync after operation. + derivedVersion: '', // Check derived changes + lastModified, + lastGithubUser: account, + ...extraSyncUpdate + }, + lastModified + ) + await this.client.diffUpdate(this.client.getHierarchy().as(doc, prj.mixinClass), update, lastModified, account) + this.provider.sync() + } + } + if (needSync) { + this.provider.sync() + } + } + + async addIssueToProject ( + container: ContainerFocus, + okit: Octokit, + issue: IssueExternalData, + projectTarget: string + ): Promise { + const query = `mutation addIssueToProject($project: ID!, $contentId: ID!) { + addProjectV2ItemById(input: {projectId: $project, contentId: $contentId}) { + item { + ${projectValue} + id + type + ${fieldValues} + } + } + }` + if (isGHWriteAllowed()) { + const response: any = await okit.graphql(query, { project: projectTarget, contentId: issue.id }) + return response.addProjectV2ItemById.item + } + } + + async removeIssueFromProject (okit: Octokit, projectTarget: string, issueId: string): Promise { + try { + const query = `mutation removeIssueToProject($project: ID!, $contentId: ID!) { + deleteProjectV2Item(input: {projectId: $project, itemId: $contentId}) { + deletedItemId + } + }` + if (isGHWriteAllowed()) { + await okit.graphql(query, { project: projectTarget, contentId: issueId }) + } + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error(err) + } + } + + findOption ( + container: ContainerFocus, + field: GithubProjectV2ItemFieldValue, + target: GithubProject | GithubMilestone + ): GithubProjectV2FieldOption | undefined { + const structure = container.container.projectStructure.get(target._id) + if (structure === undefined) { + return { + id: field.field.id, + name: field.field.name, + color: field.color ?? '', + description: field.description ?? '' + } + } + const pField = structure.projectV2.fields.edges.find((it) => it.node.id === field.field.id) + if (pField === undefined) { + return { + id: field.field.id, + name: field.field.name, + color: field.color ?? '', + description: field.description ?? '' + } + } + return (pField.node.options ?? []).find((it) => it.id === field.optionId) + } + + findOptionId (container: ContainerFocus, fieldId: string, value: string, target: IssueSyncTarget): string | undefined { + const structure = container.container.projectStructure.get(target.target._id) + if (structure === undefined) { + return + } + const pField = structure.projectV2.fields.edges.find((it) => it.node.id === fieldId) + if (pField === undefined) { + return undefined + } + return (pField.node.options ?? []).find((it) => it.name.toLowerCase() === value.toLowerCase())?.id + } + + async toPlatformField ( + container: ContainerFocus, + // eslint-disable-next-line @typescript-eslint/ban-types + field: GithubProjectV2ItemFieldValue | {}, + target: IssueSyncTarget, + taskType: TaskType + ): Promise<{ value: any, mapping: GithubFieldMapping } | undefined> { + if (!('field' in field)) { + return + } + const mapping = target.mappings.find((it) => it.githubId === field.field.id) + if (mapping === undefined) { + return undefined + } + + if (mapping.name === 'status') { + const option = this.findOption(container, field, target.target) + if (option === undefined) { + return + } + return { + value: await getCreateStatus( + this.ctx, + this.provider, + this.client, + container.project, + option?.name, + option.description, + option.color, + taskType + ), + mapping + } + } + + if (mapping.name === 'priority') { + const values: Record = { + '': IssuePriority.NoPriority, + High: IssuePriority.High, + Medium: IssuePriority.Medium, + Low: IssuePriority.Low, + Urgent: IssuePriority.Urgent + } + const option = this.findOption(container, field, target.target) + return { value: values[option?.name ?? ''] ?? IssuePriority.NoPriority, mapping } + } + + switch (field.field.dataType) { + case 'DATE': + return { value: field.date !== undefined ? new Date(field.date).getTime() : null, mapping } + case 'NUMBER': + return { value: field.number, mapping } + case 'TEXT': + return { value: field.text, mapping } + case 'SINGLE_SELECT': { + const option = this.findOption(container, field, target.target) + return { value: option?.name, mapping } + } + } + } + + async fillProjectV2Fields ( + target: IssueSyncTarget, + container: ContainerFocus, + issueData: Record, + taskType: TaskType + ): Promise { + for (const f of target.prjData?.fieldValues?.nodes ?? []) { + const ff = await this.toPlatformField(container, f, target, taskType) + if (ff === undefined) { + continue + } + const { value, mapping } = ff + if (value !== undefined) { + ;(issueData as any)[mapping.name] = value + } + } + } + + async updateIssueValues ( + target: IssueSyncTarget, + okit: Octokit, + values: { id: string, value: any, dataType: GithubDataType }[] + ): Promise<{ error: any, response: any }[]> { + function getValue (val: { id: string, value: any, dataType: GithubDataType }): string { + switch (val.dataType) { + case 'SINGLE_SELECT': + return `singleSelectOptionId: "${val.value as string}"` + case 'DATE': + return `date: "${new Date(val.value).toISOString()}"` + case 'NUMBER': + return `number: ${val.value as number}` + case 'TEXT': + return `text: "${val.value as string}"` + } + } + const errors: any[] = [] + const itm = ` { + projectV2Item { + id + type + ${projectValue} + ${fieldValues} + } + }\n` + let response: any = {} + if (isGHWriteAllowed()) { + for (const val of values) { + const q = ` + mutation updateField($project: ID!, $itemId: ID!) { + updateProjectV2ItemFieldValue( + input: {projectId: $project, itemId: $itemId, fieldId: "${val.id}", value: {${getValue(val)}}} + ) + ${itm} + }` + try { + response = await okit.graphql(q, { + project: target.target.projectNodeId, + itemId: target.prjData?.id as string + }) + } catch (err: any) { + Analytics.handleError(err) + // Failed to update one particular value, skip it. + this.ctx.error('error during field update', { error: err, response }) + errors.push({ error: err, response }) + } + } + } + return errors + } + + abstract fillBackChanges (update: DocumentUpdate, existing: GithubIssue, external: any): Promise + + async addConnectToMessage ( + msg: IntlString, + prj: Ref, + issueId: Ref, + _class: Ref>, + issueExternal: { url: string, number: number }, + repository: GithubIntegrationRepository + ): Promise { + await this.client.addCollection(activity.class.ActivityInfoMessage, prj, issueId, _class, 'activity', { + message: msg, + icon: github.icon.Github, + props: { + url: issueExternal.url, + repository: repository.url?.replace('api.github.com/repos', 'github.com'), + repoName: repository.name, + number: issueExternal.number + } + }) + } + + stripGuestLink = async (data: MarkupNode): Promise => { + await stripGuestLink(data) + } + + abstract performIssueFieldsUpdate ( + info: DocSyncInfo, + existing: Issue, + platformUpdate: DocumentUpdate, + issueData: GithubIssueData, + container: ContainerFocus, + issueExternal: IssueExternalData, + okit: Octokit, + account: Ref + ): Promise + + abstract afterSync (existing: Issue, account: Ref, issueExternal: any, info: DocSyncInfo): Promise + + async handleDiffUpdate ( + target: IssueSyncTarget, + existing: Issue, + info: DocSyncInfo, + issueData: GithubIssueData, + container: ContainerFocus, + issueExternal: IssueExternalData, + account: Ref, + accountGH: Ref, + syncToProject: boolean + ): Promise> { + let needUpdate = false + if (!this.client.getHierarchy().hasMixin(existing, github.mixin.GithubIssue)) { + await this.ctx.withLog( + 'create mixin issue: GithubIssue', + {}, + async () => { + await this.client.createMixin( + existing._id as Ref, + existing._class, + existing.space, + github.mixin.GithubIssue, + { + githubNumber: issueExternal.number, + url: issueExternal.url, + repository: info.repository as Ref + } + ) + await this.notifyConnected(container, info, existing, issueExternal) + }, + { identifier: existing.identifier, url: issueExternal.url } + ) + // Re iterate to have existing value with mixin inside. + needUpdate = true + } else { + const ghIssue = this.client.getHierarchy().as(existing, github.mixin.GithubIssue) + await this.client.diffUpdate(ghIssue, { + githubNumber: issueExternal.number, + url: issueExternal.url, + repository: info.repository as Ref + }) + if (ghIssue.url !== issueExternal.url) { + await this.notifyConnected(container, info, existing, issueExternal) + } + } + if (!this.client.getHierarchy().hasMixin(existing, container.project.mixinClass)) { + await this.ctx.withLog( + 'create mixin issue', + {}, + async () => + await this.client.createMixin( + existing._id as Ref, + existing._class, + existing.space, + container.project.mixinClass, + {} + ), + { identifier: existing.identifier, url: issueExternal.url } + ) + // Re iterate to have existing value with mixin inside. + needUpdate = true + } + if (needUpdate) { + return { needSync: '' } + } + + const existingIssue = this.client.getHierarchy().as(existing, container.project.mixinClass) + const previousData: GithubIssueData = info.current ?? ({} as unknown as GithubIssueData) + const type = await this.provider.getTaskTypeOf(container.project.type, existing._class) + const stst = await this.provider.getStatuses(type?._id) + + const update = collectUpdate(previousData, issueData, Object.keys(issueData)) + + const allAttributes = this.client.getHierarchy().getAllAttributes(container.project.mixinClass) + const platformUpdate = collectUpdate(previousData, existingIssue, Array.from(allAttributes.keys())) + + const okit = (await this.provider.getOctokit(account as Ref)) ?? container.container.octokit + + // Remove current same values from update + for (const [k, v] of Object.entries(update)) { + if ((existingIssue as any)[k] === v) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (update as any)[k] + } + } + + if (update.description !== undefined) { + if (areEqualMarkups(update.description, existingIssue.description)) { + delete update.description + } + } + + for (const [k, v] of Object.entries(update)) { + let pv = (platformUpdate as any)[k] + + if (k === 'description' && pv != null) { + const mdown = await this.provider.getMarkdown(pv) + pv = await this.provider.getMarkup(container.container, mdown, this.stripGuestLink) + } + if (pv != null && pv !== v) { + // We have conflict of values, assume platform is more proper one. + this.ctx.error('conflict', { id: existing.identifier, k }) + // Assume platform change is more important in case of conflict values. + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (update as any)[k] + continue + } + } + + await this.fillBackChanges(update, existingIssue, issueExternal) + + let needExternalSync = false + + if (container !== undefined && okit !== undefined) { + // Check and update issue fields. + needExternalSync = await this.performIssueFieldsUpdate( + info, + existing, + platformUpdate, + issueData, + container, + issueExternal, + okit, + account + ) + + const fieldsUpdate: { id: string, value: any, dataType: GithubDataType }[] = [] + + // Collect field update. + for (const [k, v] of Object.entries(platformUpdate)) { + const mapping = target.mappings.find((it) => it.name === k) + if (mapping === undefined) { + continue + } + const attr = this.client.getHierarchy().getAttribute(mapping._class, mapping.name) + + if (attr.name === 'status') { + // Handle status field + const status = stst.find((it) => it._id === v) as Status + const optionId = this.findOptionId(container, mapping.githubId, status.name, target) + if (optionId !== undefined) { + fieldsUpdate.push({ + id: mapping.githubId, + dataType: 'SINGLE_SELECT', + value: optionId + }) + this.ctx.info(' => prepare issue status update', { + url: issueExternal.url, + name: status.name, + workspace: this.provider.getWorkspaceId().name + }) + continue + } + } + if (attr.name === 'priority') { + const values: Record = { + [IssuePriority.NoPriority]: '', + [IssuePriority.High]: 'High', + [IssuePriority.Medium]: 'Medium', + [IssuePriority.Low]: 'Low', + [IssuePriority.Urgent]: 'Urgent' + } + // Handle priority field TODO: Add clear of field + const priorityName = values[v as IssuePriority] + const optionId = this.findOptionId(container, mapping.githubId, priorityName, target) + if (optionId !== undefined) { + fieldsUpdate.push({ + id: mapping.githubId, + dataType: 'SINGLE_SELECT', + value: optionId + }) + this.ctx.info(' => prepare issue priority update', { + url: issueExternal.url, + priority: priorityName, + workspace: this.provider.getWorkspaceId().name + }) + continue + } + } + + const dataType = getType(attr) + if (dataType === 'SINGLE_SELECT') { + // Handle status field + const optionId = this.findOptionId(container, mapping.githubId, v, target) + if (optionId !== undefined) { + fieldsUpdate.push({ + id: mapping.githubId, + dataType: 'SINGLE_SELECT', + value: optionId + }) + this.ctx.info(` => prepare issue field ${attr.label} update`, { + url: issueExternal.url, + value: v, + workspace: this.provider.getWorkspaceId().name + }) + continue + } + } + + if (dataType === undefined) { + continue + } + fieldsUpdate.push({ + id: mapping.githubId, + dataType, + value: v + }) + this.ctx.info(`=> prepare issue field ${attr.label} update`, { + url: issueExternal.url, + value: v, + workspace: this.provider.getWorkspaceId().name + }) + } + if (fieldsUpdate.length > 0 && syncToProject && target.prjData !== undefined) { + const errors = await this.updateIssueValues(target, okit, fieldsUpdate) + if (errors.length > 0) { + return { externalVersion: '', needUpdate: githubSyncVersion, error: errors } + } + needExternalSync = true + } + // TODO: Add support for labels, milestone, assignees + } + + // We need remove all readonly field values + for (const k of Object.keys(update)) { + // Skip readonly fields + const attr = this.client.getHierarchy().findAttribute(target.project.mixinClass, k) + if (attr?.readonly === true) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (update as any)[k] + continue + } + } + + if (Object.keys(update).length > 0) { + // Update collaborative description + this.ctx.info(`<= perform ${issueExternal.url} update to collaborator`, { + workspace: this.provider.getWorkspaceId().name + }) + if (update.description !== undefined) { + try { + issueData.description = update.description + const collaborativeDoc = getCollaborativeDoc(getCollaborativeDocId(existingIssue._id, 'description')) + await this.collaborator.updateContent(collaborativeDoc, 'description', update.description) + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error('error during description update', err) + } + } + // We have some fields to update of existing from external + this.ctx.info(`<= perform ${issueExternal.url} update to platform`, { + ...update, + workspace: this.provider.getWorkspaceId().name + }) + await this.client.update(existingIssue, update, false, new Date().getTime(), accountGH) + } + + await this.afterSync(existingIssue, accountGH, issueExternal, info) + // We need to trigger external version retrieval, via sync or event, to prevent move sync operations from platform before we will be sure all is updated on github. + return { + current: issueData, + needSync: githubSyncVersion, + ...(needExternalSync ? { externalVersion: '' } : {}), + lastGithubUser: null + } + } + + private async notifyConnected ( + container: ContainerFocus, + info: DocSyncInfo, + existing: Issue, + issueExternal: IssueExternalData + ): Promise { + const repo = container.repository.find((it) => it._id === info.repository) as GithubIntegrationRepository + await this.addConnectToMessage( + existing._class === github.class.GithubPullRequest + ? github.string.PullRequestConnectedActivityInfo + : github.string.IssueConnectedActivityInfo, + existing.space, + existing._id, + existing._class, + issueExternal, + repo + ) + } + + async collectIssueUpdate ( + info: DocSyncInfo, + doc: Issue, + platformUpdate: DocumentUpdate, + issueData: Pick, + container: ContainerFocus, + issueExternal: IssueExternalData, + _class: Ref> + ): Promise> { + const issueUpdate: { + title?: string + body?: string + stateReason?: string + assigneeIds?: string[] + } & Record = {} + if (platformUpdate.title != null) { + if (platformUpdate.title !== issueExternal.title) { + issueUpdate.title = platformUpdate.title + } + issueData.title = platformUpdate.title + } + if (platformUpdate.description != null) { + // Need to convert to markdown + const pp = async (nodes: MarkupNode): Promise => { + await appendGuestLink(this.client, doc, nodes, this.provider.getWorkspaceId(), this.provider.getBranding()) + } + issueUpdate.body = await this.provider.getMarkdown( + platformUpdate.description ?? '', + info.allowOpenInHuly === true ? pp : undefined + ) + issueData.description = await this.provider.getMarkup( + container.container, + issueUpdate.body ?? '', + this.stripGuestLink + ) + + // Of value is same, not need to update. + if (compareMarkdown(issueUpdate.body, issueExternal.body)) { + delete issueUpdate.body + } + } + if (platformUpdate.assignee !== undefined) { + const info = + platformUpdate.assignee !== null + ? await this.provider.getGithubLogin(container.container, platformUpdate.assignee) + : undefined + // Check external + + const currentAssignees = issueExternal.assignees.nodes.map((it) => it.id) + currentAssignees.sort((a, b) => a.localeCompare(b)) + + issueUpdate.assigneeIds = info !== undefined ? [info.id] : [] + issueUpdate.assigneeIds.sort((a, b) => a.localeCompare(b)) + + if (deepEqual(currentAssignees, issueUpdate.assigneeIds)) { + // Same ids + delete issueUpdate.assigneeIds + } + issueData.assignee = platformUpdate.assignee + } + + const status = platformUpdate.status ?? issueData.status + const type = await this.provider.getTaskTypeOf(container.project.type, _class) + const statuses = await this.provider.getStatuses(type?._id) + const st = statuses.find((it) => it._id === status) + if (st !== undefined) { + // Need to convert to two operations. + switch (st.category) { + case task.statusCategory.UnStarted: + case task.statusCategory.ToDo: + case task.statusCategory.Active: + if (issueExternal.state !== 'OPEN') { + issueUpdate.state = 'OPEN' + } + break + case task.statusCategory.Won: + if (issueExternal.state !== 'CLOSED') { + issueUpdate.state = 'CLOSED' + } + break + case task.statusCategory.Lost: + if (issueExternal.state !== 'CLOSED') { + issueUpdate.state = 'CLOSED' + // issueUpdate.stateReason = 'not_planed'// Not supported change to github + } + break + } + } + return issueUpdate + } + + async syncIssues ( + _class: Ref>, + repo: GithubIntegrationRepository, + issues: IssueExternalData[], + derivedClient: TxOperations + ): Promise { + if (repo.githubProject == null) { + return + } + const syncInfo = await this.client.findAll(github.class.DocSyncInfo, { + space: repo.githubProject, + repository: repo._id, + objectClass: _class, + url: { $in: issues.map((it) => (it.url ?? '').toLowerCase()) } + }) + + const ops = derivedClient.apply('sync-issyes' + generateId()) + + for (const issue of issues) { + try { + if (issue.url === undefined && Object.keys(issue).length === 0) { + this.ctx.info('Retrieve empty document', { repo: repo.name, workspace: this.provider.getWorkspaceId().name }) + continue + } + const existing = syncInfo.find((it) => it.url === issue.url.toLowerCase()) + if (existing === undefined) { + this.ctx.info('Create sync doc', { url: issue.url, workspace: this.provider.getWorkspaceId().name }) + await ops.createDoc(github.class.DocSyncInfo, repo.githubProject, { + url: issue.url.toLowerCase(), + needSync: '', + repository: repo._id, + githubNumber: issue.number, + objectClass: _class, + external: issue, + externalVersion: githubExternalSyncVersion, + derivedVersion: '', + externalVersionSince: '', + lastModified: new Date(issue.updatedAt).getTime() + }) + } else { + const externalEqual = deepEqual(existing.external, issue) + if (!externalEqual || existing.externalVersion !== githubExternalSyncVersion) { + this.ctx.info('Update sync doc', { url: issue.url, workspace: this.provider.getWorkspaceId().name }) + await ops.diffUpdate( + existing, + { + needSync: externalEqual ? existing.needSync : '', + external: issue, + externalVersion: githubExternalSyncVersion, + derivedVersion: '', // Clear derived state to recalculate it. + externalVersionSince: '', + lastModified: new Date(issue.updatedAt).getTime() + }, + Date.now() + ) + } + } + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error(err) + } + } + await ops.commit(true) + this.provider.sync() + } + + async getMilestoneIssueTarget ( + project: GithubProject, + container: IntegrationContainer, + existingIssue: Issue | undefined, + external: IssueExternalData + ): Promise { + if (existingIssue !== undefined) { + // Select a milestone project + if (existingIssue.milestone != null) { + const milestone = ( + await this.provider.liveQuery.queryFind(github.mixin.GithubMilestone, {}) + ).find((it) => it._id === existingIssue.milestone) + if (milestone === undefined) { + // Let's search for milestone, and if it doesn't have mixin, return undefined. + const mstone = await this.client.findOne(github.mixin.GithubMilestone, { + _id: existingIssue.milestone as Ref + }) + if (mstone === undefined) { + return undefined + } + return null + } + return { + project, + mappings: milestone.mappings, + target: milestone, + prjData: external.projectItems.nodes.find((it) => it.project.id === milestone.projectNodeId) + } + } + } + } + + getProjectIssueTarget (project: GithubProject, external?: IssueExternalData): IssueSyncTarget { + return { + project, + mappings: project.mappings, + target: project, + prjData: external?.projectItems.nodes.find((it) => it.project.id === project.projectNodeId) + } + } + + abstract deleteGithubDocument (container: ContainerFocus, account: Ref, id: string): Promise + + async handleDelete ( + existing: Doc | undefined, + info: DocSyncInfo, + derivedClient: TxOperations, + deleteExisting: boolean + ): Promise { + const container = await this.provider.getContainer(info.space) + if (container === undefined) { + return false + } + if ( + container?.container === undefined || + ((container.project.projectNodeId === undefined || + !container.container.projectStructure.has(container.project._id)) && + syncConfig.MainProject) + ) { + return false + } + + const issueExternal = info.external as IssueExternalData | undefined + + if (issueExternal === undefined) { + // No external issue yet, safe delete, since platform document will be deleted a well. + return true + } + const account = + existing?.createdBy ?? (await this.provider.getAccount(issueExternal.author))?._id ?? core.account.System + const okit = (await this.provider.getOctokit(account as Ref)) ?? container.container.octokit + + if (existing !== undefined && issueExternal !== undefined) { + let target = await this.getMilestoneIssueTarget( + container.project, + container.container, + existing as Issue, + issueExternal + ) + if (target === null) { + // We need to wait, no milestone data yet. + return false + } + if (target === undefined) { + target = this.getProjectIssueTarget(container.project, issueExternal) + } + const isProjectProjectTarget = target.target.projectNodeId === target.project.projectNodeId + const supportProjects = + (isProjectProjectTarget && syncConfig.MainProject) || (!isProjectProjectTarget && syncConfig.SupportMilestones) + + // A target node id + const targetNodeId: string | undefined = info.targetNodeId as string + + if (targetNodeId !== undefined && supportProjects) { + const itemNode = issueExternal.projectItems.nodes.find((it) => it.project.id === targetNodeId) + if (itemNode !== undefined) { + await this.removeIssueFromProject(okit, targetNodeId, itemNode.id) + } + // Clear external project items + info.external.projectItems = [] + } + } + + if (issueExternal !== undefined) { + try { + await this.deleteGithubDocument(container, account, issueExternal.id) + } catch (err: any) { + let cnt = false + if (Array.isArray(err.errors)) { + for (const e of err.errors) { + if (e.type === 'NOT_FOUND') { + // Ok issue is already deleted + cnt = true + break + } + } + } + if (!cnt) { + Analytics.handleError(err) + this.ctx.error('Error', { err }) + await derivedClient.update(info, { error: errorToObj(err) }) + } + } + } + + if (existing !== undefined && deleteExisting) { + const childItems = await derivedClient.findAll(github.class.DocSyncInfo, { + parentUrl: (issueExternal.url ?? '').toLowerCase() + }) + for (const u of childItems) { + // We need just to clean all of them, since child's for issue are comments for now. + await derivedClient.remove(u) + } + + await deleteObjects(this.ctx, this.client, [existing], account) + } + return true + } + + protected async createErrorSyncDataByUrl ( + url: string, + githubNumber: number, + date: Date, + derivedClient: TxOperations, + repo: GithubIntegrationRepository, + err: any, + _class: Ref> = tracker.class.Issue + ): Promise { + const syncData = await this.client.findOne(github.class.DocSyncInfo, { url: url.toLowerCase() }) + if (syncData === undefined) { + await derivedClient?.createDoc(github.class.DocSyncInfo, repo.githubProject as Ref, { + url, + needSync: githubSyncVersion, // We need external sync first. + githubNumber, + externalVersion: '', + repository: repo._id, + objectClass: _class, + external: {}, + error: errorToObj(err), + lastModified: date.getTime() + }) + // We need trigger comments, if their sync data created before + const childInfos = await this.client.findAll(github.class.DocSyncInfo, { parent: url.toLowerCase() }) + for (const child of childInfos) { + await derivedClient?.update(child, { needSync: '' }) + } + this.provider.sync() + } + } +} diff --git a/services/github/pod-github/src/sync/issues.ts b/services/github/pod-github/src/sync/issues.ts new file mode 100644 index 0000000000..4997862bb3 --- /dev/null +++ b/services/github/pod-github/src/sync/issues.ts @@ -0,0 +1,1089 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +/* + TODO: + * Add since to synchronization +*/ +import { Analytics } from '@hcengineering/analytics' +import { PersonAccount } from '@hcengineering/contact' +import core, { + Account, + AttachedData, + Doc, + DocumentUpdate, + Ref, + SortingOrder, + Status, + TxOperations, + cutObjectArray, + generateId, + getCollaborativeDoc, + getCollaborativeDocId +} from '@hcengineering/core' +import task, { TaskType, calcRank } from '@hcengineering/task' +import { isEmptyMarkup } from '@hcengineering/text' +import tracker, { Issue, IssuePriority } from '@hcengineering/tracker' +import { Issue as GithubIssue, IssuesEvent, ProjectsV2ItemEvent } from '@octokit/webhooks-types' +import github, { + DocSyncInfo, + GithubIntegrationRepository, + GithubIssueStateReason, + GithubProject, + IntegrationRepositoryData, + GithubIssue as TGithubIssue +} from '@hcengineering/github' +import { Octokit } from 'octokit' +import config from '../config' +import { + ContainerFocus, + DocSyncManager, + ExternalSyncField, + IntegrationContainer, + githubDerivedSyncVersion, + githubExternalSyncVersion, + githubSyncVersion +} from '../types' +import { IssueExternalData, issueDetails } from './githubTypes' +import { appendGuestLink } from './guest' +import { GithubIssueData, IssueSyncManagerBase, IssueSyncTarget } from './issueBase' +import { syncConfig } from './syncConfig' +import { getSince, gqlp, guessStatus, isGHWriteAllowed, syncRunner } from './utils' + +export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncManager { + createPromise: Promise | undefined + externalDerivedSync = false + async getAssigneesI (issue: GithubIssue): Promise { + // Find Assignees and reviewers + const assignees: PersonAccount[] = [] + + for (const o of issue.assignees) { + const acc = await this.provider.getAccountU(o) + if (acc !== undefined) { + assignees.push(acc) + } + } + return assignees + } + + async handleEvent( + integration: IntegrationContainer, + derivedClient: TxOperations, + evt: T + ): Promise { + await this.createPromise + const event = evt as IssuesEvent | ProjectsV2ItemEvent + + this.ctx.info('issue:handleEvent', { + nodeId: (event as IssuesEvent).issue?.html_url ?? (event as ProjectsV2ItemEvent)?.projects_v2_item.node_id, + action: event.action, + login: event.sender.login, + type: event.sender.type, + url: event.sender.url, + workspace: this.provider.getWorkspaceId().name + }) + + if (event.sender.type === 'Bot') { + // Ignore events from Bot if it is our bot + // No need to handle event from ourself + if (event.sender.login.includes(config.BotName)) { + return + } + } + + const projectV2Event = (event as ProjectsV2ItemEvent).projects_v2_item?.id !== undefined + if (projectV2Event) { + const projectV2Event = event as ProjectsV2ItemEvent + + const githubProjects = await this.provider.liveQuery.queryFind(github.mixin.GithubProject, {}) + let prj = githubProjects.find((it) => it.projectNodeId === projectV2Event.projects_v2_item.project_node_id) + if (prj === undefined) { + // Checking for milestones + const m = (await this.provider.liveQuery.queryFind(github.mixin.GithubMilestone, {})).find( + (it) => it.projectNodeId === projectV2Event.projects_v2_item.project_node_id + ) + if (m !== undefined) { + prj = githubProjects.find((it) => it._id === m.space) + } + } + + if (prj === undefined) { + this.ctx.info('Event from unknown v2 project', { + nodeId: projectV2Event.projects_v2_item.project_node_id, + workspace: this.provider.getWorkspaceId().name + }) + return + } + + const urlId = projectV2Event.projects_v2_item.node_id + + await syncRunner.exec(urlId, async () => { + await this.processProjectV2Event(integration, projectV2Event, derivedClient, prj as GithubProject) + }) + } else { + const issueEvent = event as IssuesEvent + const { project, repository } = await this.provider.getProjectAndRepository(issueEvent.repository.node_id) + if (project === undefined || repository === undefined) { + this.ctx.info('No project for repository', { + repository: issueEvent.repository.name, + nodeId: issueEvent.repository.node_id, + workspace: this.provider.getWorkspaceId().name + }) + return + } + + const urlId = issueEvent.issue.url + + await syncRunner.exec(urlId, async () => { + await this.processEvent(issueEvent, derivedClient, repository, integration, project) + }) + } + } + + private async processEvent ( + event: IssuesEvent, + derivedClient: TxOperations, + repo: GithubIntegrationRepository, + integration: IntegrationContainer, + prj: GithubProject + ): Promise { + const account = (await this.provider.getAccountU(event.sender))?._id ?? core.account.System + + let externalData: IssueExternalData | undefined + if (event.action !== 'deleted') { + try { + const response: any = await integration.octokit?.graphql( + `query listIssue($name: String!, $owner: String!, $issue: Int!) { + repository(name: $name, owner: $owner) { + issue(number: $issue) { + ${issueDetails(true)} + } + } + } + `, + { + name: repo.name, + owner: repo.owner?.login, + issue: event.issue.number + } + ) + externalData = response.repository.issue + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error('Error', { err }) + + // We need to check if we do not have sync data, we need to create by html_url + await this.createErrorSyncDataByUrl( + event.issue.html_url, + event.issue.number, + new Date(event.issue.updated_at), + derivedClient, + repo, + err + ) + return + } + if (externalData === undefined) { + await this.createErrorSyncDataByUrl( + event.issue.html_url, + event.issue.number, + new Date(event.issue.updated_at), + derivedClient, + repo, + 'no external data found' + ) + return + } + } + + switch (event.action) { + case 'transferred': + case 'opened': { + await this.createSyncData(externalData as IssueExternalData, derivedClient, repo) + break + } + case 'deleted': { + const syncData = await this.client.findOne(github.class.DocSyncInfo, { + url: (event.issue.html_url ?? '').toLowerCase() + }) + if (syncData !== undefined) { + await derivedClient.update(syncData, { deleted: true, needSync: '', lastGithubUser: account }) + this.provider.sync() + } + break + } + case 'edited': { + const update: DocumentUpdate = {} + const du: DocumentUpdate = {} + if (event.changes.body !== undefined) { + update.description = await this.provider.getMarkup(integration, event.issue.body, this.stripGuestLink) + du.markdown = await this.provider.getMarkdown(update.description) + } + if (event.changes.title !== undefined) { + update.title = event.issue.title + } + + await this.handleUpdate( + externalData as IssueExternalData, + derivedClient, + update, + account, + prj, + false, + undefined, + undefined, + du + ) + break + } + case 'assigned': + case 'unassigned': { + const assignees = await this.getAssigneesI(event.issue) + const update: DocumentUpdate = { + assignee: assignees?.[0]?.person ?? null + } + await this.handleUpdate(externalData as IssueExternalData, derivedClient, update, account, prj, false) + break + } + case 'closed': + case 'reopened': { + const stateMap: Record = { closed: 'CLOSED', open: 'OPEN' } + const stateReasonMap: Record = { + completed: GithubIssueStateReason.Completed, + reopened: GithubIssueStateReason.Reopened, + not_planed: GithubIssueStateReason.NotPlanned + } + const type = await this.provider.getTaskTypeOf(prj.type, tracker.class.Issue) + const statuses = await this.provider.getStatuses(type?._id) + const update: DocumentUpdate = { + status: ( + await guessStatus( + { + state: stateMap[event.issue.state], + stateReason: event.issue.state_reason != null ? stateReasonMap[event.issue.state_reason] : undefined + }, + statuses + ) + )._id + } + await this.handleUpdate( + externalData as IssueExternalData, + derivedClient, + update, + account, + prj, + false, + undefined, + async (state, existing, external) => { + // We need to be sure we not change status if category is same, since github doesn't know about it. + const existingStatus = statuses.find((it) => it._id === existing.status) + const updateState = statuses.find((it) => it._id === update.status) + return existingStatus?.category !== updateState?.category + } + ) + break + } + } + } + + private async createSyncData ( + issueExternal: IssueExternalData, + derivedClient: TxOperations | undefined, + repo: GithubIntegrationRepository + ): Promise { + const issueSyncData = await this.client.findOne(github.class.DocSyncInfo, { + url: (issueExternal.url ?? '').toLowerCase() + }) + if (issueSyncData === undefined) { + await derivedClient?.createDoc(github.class.DocSyncInfo, repo.githubProject as Ref, { + url: issueExternal.url.toLowerCase(), + needSync: '', + githubNumber: issueExternal.number, + repository: repo._id, + objectClass: tracker.class.Issue, + external: issueExternal, + externalVersion: githubExternalSyncVersion, + lastModified: new Date(issueExternal.updatedAt).getTime(), + allowOpenInHuly: true + }) + + // We need trigger comments, if their sync data created before + const childInfos = await this.client.findAll(github.class.DocSyncInfo, { + parent: (issueExternal.url ?? '').toLowerCase() + }) + for (const child of childInfos) { + await derivedClient?.update(child, { needSync: '' }) + } + + this.provider.sync() + } + } + + async sync ( + existing: Doc | undefined, + info: DocSyncInfo, + parent: DocSyncInfo | undefined, + derivedClient: TxOperations + ): Promise | undefined> { + const container = await this.provider.getContainer(info.space) + if ( + container?.container === undefined || + ((container.project.projectNodeId === undefined || + !container.container.projectStructure.has(container.project._id)) && + syncConfig.MainProject) + ) { + this.ctx.error('Not syncing no structure', { url: info.url }) + return { needSync: '' } + } + + if (info.repository == null && existing !== undefined) { + if (this.client.getHierarchy().hasMixin(existing, github.mixin.GithubIssue)) { + const repositoryId = this.client.getHierarchy().as(existing, github.mixin.GithubIssue).repository + + if (repositoryId !== undefined) { + info.repository = repositoryId + await derivedClient.update(info, { repository: repositoryId }) + } + if (info.repository == null) { + // No need to sync if component it not yet set + const repos = container.repository.map((it) => it.name).join(', ') + this.ctx.error('Not syncing repository === null', { + url: info.url, + identifier: (existing as Issue).identifier, + repos + }) + return { needSync: githubSyncVersion } + } + } + } + + let externalWasCreated = false + + let issueExternal = info.external as IssueExternalData + if (info.external === undefined && existing !== undefined) { + const repository = container.repository.find((it) => it._id === info.repository) + if (repository === undefined) { + const repos = container.repository.map((it) => it.name).join(', ') + this.ctx.error('Not syncing repository === undefined', { + url: info.url, + identifier: (existing as Issue).identifier, + repos + }) + return { needSync: githubSyncVersion } + } + + this.ctx.info('create github issue', { + title: (existing as Issue).title, + number: (existing as Issue).number, + workspace: this.provider.getWorkspaceId().name + }) + const createdIssueData = await this.ctx.withLog( + 'create github issue', + {}, + async () => { + this.createPromise = this.createGithubIssue(container, existing as Issue, repository) + return await this.createPromise + }, + { id: (existing as Issue).identifier, workspace: this.provider.getWorkspaceId().name } + ) + if (createdIssueData === undefined) { + this.ctx.error('Error create issue', { url: info.url }) + return { needSync: githubSyncVersion, error: 'Unknown error on create issue' } + } + issueExternal = createdIssueData + + // Store external value created. + const update: DocumentUpdate = { + external: issueExternal, + externalVersion: githubExternalSyncVersion, + url: issueExternal.url.toLowerCase(), + githubNumber: issueExternal.number, + lastModified: new Date(issueExternal.updatedAt).getTime(), + allowOpenInHuly: true, + current: { + title: issueExternal.title, + description: await this.provider.getMarkup(container.container, issueExternal.body, this.stripGuestLink) + } + } + await derivedClient.update(info, update) + info.external = update.external + info.externalVersion = update.externalVersion + info.current = update.current + + externalWasCreated = true + } + if (info.externalVersion !== githubExternalSyncVersion) { + // We wait external sync. + return { needSync: githubSyncVersion } + } + + let target = await this.getMilestoneIssueTarget( + container.project, + container.container, + existing as Issue, + issueExternal + ) + if (target === null) { + // We need to wait, no milestone data yet. + this.ctx.error('target === null', { url: info.url }) + return { needSync: '' } + } + if (target === undefined) { + target = this.getProjectIssueTarget(container.project, issueExternal) + } + + const syncResult = await this.syncToTarget(target, container, existing, issueExternal, derivedClient, info) + + if (externalWasCreated && existing !== undefined) { + // Create child documents + const createId = generateId() + while (true) { + const attachedDocs = await this.client.findAll( + github.class.DocSyncInfo, + { attachedTo: existing._id, createId: { $ne: createId } }, + { sort: { createdOn: SortingOrder.Ascending }, limit: 50 } + ) + if (attachedDocs.length === 0) { + break + } + + await this.provider.doSyncFor(attachedDocs) + for (const child of attachedDocs) { + await derivedClient.update(child, { createId }) + } + } + } + return { + ...syncResult, + issueExternal, + targetNodeId: target.target.projectNodeId + } + } + + async syncToTarget ( + target: IssueSyncTarget, + container: ContainerFocus, + existing: Doc | undefined, + issueExternal: IssueExternalData, + derivedClient: TxOperations, + info: DocSyncInfo + ): Promise> { + const account = + existing?.modifiedBy ?? (await this.provider.getAccount(issueExternal.author))?._id ?? core.account.System + const accountGH = + info.lastGithubUser ?? (await this.provider.getAccount(issueExternal.author))?._id ?? core.account.System + + const isProjectProjectTarget = target.target.projectNodeId === target.project.projectNodeId + const supportProjects = + (isProjectProjectTarget && syncConfig.MainProject) || (!isProjectProjectTarget && syncConfig.SupportMilestones) + + // A target node id + const targetNodeId: string | undefined = info.targetNodeId as string + + const okit = (await this.provider.getOctokit(account as Ref)) ?? container.container.octokit + + const type = await this.provider.getTaskTypeOf(container.project.type, tracker.class.Issue) + const statuses = await this.provider.getStatuses(type?._id) + // collaborators: assignees.map((it) => it._id), + const assignees = await this.getAssignees(issueExternal) + + const issueData = { + title: issueExternal.title, + description: await this.provider.getMarkup(container.container, issueExternal.body, this.stripGuestLink), + assignee: assignees[0]?.person, + repository: info.repository, + remainingTime: 0 + } + + const taskTypes = (await this.client.findAll(task.class.TaskType, { parent: container.project.type })).filter( + (it) => + this.client.getHierarchy().isDerived(it.targetClass, tracker.class.Issue) && + !this.client.getHierarchy().isDerived(it.targetClass, github.class.GithubPullRequest) + ) + + // TODO: Use GithubProject configuration to specify target type for issues + if (taskTypes.length === 0) { + // Missing required task type + this.ctx.error('Missing required task type', { identifier: (existing as Issue)?.identifier }) + return { needSync: githubSyncVersion } + } + + await this.fillProjectV2Fields(target, container, issueData, taskTypes[0]) + + if ( + targetNodeId !== undefined && + target.target.projectNodeId !== undefined && + targetNodeId !== target.target.projectNodeId && + supportProjects + ) { + const itemNode = issueExternal.projectItems.nodes.find((it) => it.project.id === targetNodeId) + if (itemNode !== undefined) { + await this.removeIssueFromProject(okit, targetNodeId, itemNode.id) + // remove data + issueExternal.projectItems.nodes = issueExternal.projectItems.nodes.filter((it) => it.id !== targetNodeId) + target.prjData = undefined + await derivedClient.update(info, { + external: issueExternal, + externalVersion: githubExternalSyncVersion + }) + // We need to sync from platform as new to new project. + // We need to remove current sync + info.current = {} + } + } + + if (target.prjData === undefined && okit !== undefined && syncConfig.IssuesInProject && supportProjects) { + try { + this.ctx.info('add issue to project v2', { + url: issueExternal.url, + workspace: this.provider.getWorkspaceId().name + }) + target.prjData = await this.ctx.withLog( + 'add issue to project v2', + {}, + async () => + await this.addIssueToProject(container, okit, issueExternal, target.target.projectNodeId as string) + ) + if (target.prjData !== undefined) { + issueExternal.projectItems.nodes.push(target.prjData) + } + + await derivedClient.update(info, { + external: issueExternal, + externalVersion: githubExternalSyncVersion + }) + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error('Error add project v2', { err }) + return { needSync: githubSyncVersion, error: JSON.stringify(err) } + } + } + if (existing === undefined) { + try { + this.ctx.info('create platform issue', { + url: issueExternal.url, + title: issueExternal.title, + workspace: this.provider.getWorkspaceId().name + }) + const { markdownCompatible, markdown } = await this.provider.checkMarkdownConversion( + container.container, + issueExternal.body + ) + await this.ctx.withLog( + 'create platform issue', + {}, + async () => { + const st = (await guessStatus(issueExternal, statuses))._id as Ref + + await this.createNewIssue( + info, + accountGH, + { + ...issueData, + status: st + }, + issueExternal, + info.repository as Ref, + container.project, + taskTypes[0]._id, + container.repository.find((it) => it._id === info.repository) as GithubIntegrationRepository & { + repository: IntegrationRepositoryData + }, + !markdownCompatible + ) + }, + { url: issueExternal.url } + ) + // We need reiterate to update all sync data. + return { + needSync: '', + external: issueExternal, + externalVersion: githubExternalSyncVersion, + lastModified: new Date(issueExternal.updatedAt).getTime(), + lastGithubUser: null, + isDescriptionLocked: !markdownCompatible, + markdown + } + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error('Error', { err }) + return { needSync: githubSyncVersion, error: JSON.stringify(err) } + } + } else { + try { + const collaborativeDoc = getCollaborativeDoc(getCollaborativeDocId(existing._id, 'description')) + const description = await this.ctx.withLog( + 'query collaborative description', + {}, + async () => { + const content = await this.collaborator.getContent(collaborativeDoc, 'description') + return isEmptyMarkup(content) ? (existing as Issue).description : content + }, + { url: issueExternal.url } + ) + + const updateResult = await this.ctx.withLog( + 'diff update', + {}, + async () => + await this.handleDiffUpdate( + target, + { ...(existing as any), description }, + info, + issueData, + container, + issueExternal, + account, + accountGH, + supportProjects + ), + { url: issueExternal.url } + ) + return { + ...updateResult, + lastModified: new Date(issueExternal.updatedAt).getTime(), + lastGithubUser: null + } + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error('error sync', { err }) + return { needSync: githubSyncVersion, error: JSON.stringify(err), external: issueExternal } + } + } + } + + async afterSync (existing: Issue, update: DocumentUpdate, account: Ref): Promise {} + + async performIssueFieldsUpdate ( + info: DocSyncInfo, + existing: Issue, + platformUpdate: DocumentUpdate, + issueData: Pick, + container: ContainerFocus, + issueExternal: IssueExternalData, + okit: Octokit, + account: Ref + ): Promise { + const { state, body, ...issueUpdate } = await this.collectIssueUpdate( + info, + existing, + platformUpdate, + issueData, + container, + issueExternal, + tracker.class.Issue + ) + + const isLocked = + info.isDescriptionLocked === true && !(await this.provider.isPlatformUser(account as Ref)) + + const hasFieldStateChanges = Object.keys(issueUpdate).length > 0 || state !== undefined + // We should allow modification from user. + + if (hasFieldStateChanges || body !== undefined) { + if (body !== undefined && !isLocked) { + await this.ctx.withLog( + '==> updateIssue', + {}, + async () => { + this.ctx.info('update fields', { + url: issueExternal.url, + ...issueUpdate, + body, + workspace: this.provider.getWorkspaceId().name + }) + if (isGHWriteAllowed()) { + await okit?.graphql( + ` + mutation updateIssue($issue: ID!, $body: String! ) { + updateIssue(input: { + id: $issue, + ${state !== undefined ? `state: ${state as string}` : ''} + ${gqlp(issueUpdate)}, + body: $body + }) { + issue { + id + updatedAt + } + } + }`, + { issue: issueExternal.id, body } + ) + } + }, + { url: issueExternal.url, id: existing._id } + ) + issueData.description = await this.provider.getMarkup(container.container, body, this.stripGuestLink) + } else if (hasFieldStateChanges) { + await this.ctx.withLog( + '==> updateIssue', + {}, + async () => { + this.ctx.info('update fields', { ...issueUpdate, workspace: this.provider.getWorkspaceId().name }) + if (isGHWriteAllowed()) { + await okit?.graphql( + ` + mutation updateIssue($issue: ID!) { + updateIssue(input: { + id: $issue, + ${state !== undefined ? `state: ${state as string}` : ''} + ${gqlp(issueUpdate)} + }) { + issue { + id + updatedAt + } + } + }`, + { issue: issueExternal.id } + ) + } + }, + { url: issueExternal.url } + ) + } + return true + } + return false + } + + async createGithubIssue ( + container: ContainerFocus, + existing: Issue, + repository: GithubIntegrationRepository + ): Promise { + const existingIssue = existing + + const okit = + (await this.provider.getOctokit(existingIssue.modifiedBy as Ref)) ?? container.container.octokit + + const repoId = repository.nodeId + + const info = + existingIssue.assignee !== null + ? await this.provider.getGithubLogin(container.container, existingIssue.assignee) + : undefined + + const assigneeIds = info !== undefined ? [info.id] : [] + + const q = `mutation createIssue($repo:ID!, $title: String!, $body: String, $assigneeIds: [ID!]) { + createIssue( + input: {repositoryId: $repo, title: $title, body: $body, assigneeIds: $assigneeIds} + ) { + issue { + ${issueDetails(true)} + } + } + }` + + const body = + (await this.provider.getMarkdown(existingIssue.description, async (nodes) => { + await appendGuestLink(this.client, existing, nodes, this.provider.getWorkspaceId(), this.provider.getBranding()) + })) ?? '' + if (isGHWriteAllowed()) { + const response: + | { + createIssue: { + issue: IssueExternalData + } + } + | undefined = await okit?.graphql(q, { + repo: repoId, + title: existingIssue.title, + body, + assigneeIds + }) + + return response?.createIssue?.issue + } + } + + async deleteGithubDocument (container: ContainerFocus, account: Ref, id: string): Promise { + const okit = (await this.provider.getOctokit(account as Ref)) ?? container.container.octokit + + const q = `mutation deleteIssue($issueID: ID!) { + deleteIssue( + input: {issueId: $issueID} + ) { + repository { + url + } + } + }` + if (isGHWriteAllowed()) { + await okit?.graphql(q, { + issueID: id + }) + } + } + + private async createNewIssue ( + info: DocSyncInfo, + account: Ref, + issueData: GithubIssueData & { status: Issue['status'] }, + issueExternal: IssueExternalData, + repo: Ref, + prj: GithubProject, + taskType: Ref, + repository: GithubIntegrationRepository, + isDescriptionLocked: boolean + ): Promise { + const lastOne = await this.client.findOne( + tracker.class.Issue, + { space: prj._id }, + { sort: { rank: SortingOrder.Descending } } + ) + const incResult = await this.client.updateDoc( + tracker.class.Project, + core.space.Space, + prj._id, + { $inc: { sequence: 1 } }, + true, + new Date().getTime(), + account + ) + + const number = (incResult as any).object.sequence + + const value: AttachedData = { + ...issueData, + kind: taskType, + component: null, + milestone: null, + number, + priority: IssuePriority.NoPriority, + rank: calcRank(lastOne, undefined), + comments: 0, + subIssues: 0, + dueDate: null, + parents: [], + reportedTime: 0, + remainingTime: 0, + estimation: 0, + reports: 0, + relations: [], + childInfo: [], + identifier: `${prj.identifier}-${number}` + } + const issueId = info._id as unknown as Ref + + await this.client.addCollection( + tracker.class.Issue, + prj._id, + tracker.ids.NoParent, + tracker.class.Issue, + 'subIssues', + value, + issueId, + new Date(issueExternal.createdAt).getTime(), + account + ) + + // Compare description and our markdown and mark issue as description locked in case they are not match. + + await this.client.createMixin(issueId, tracker.class.Issue, prj._id, github.mixin.GithubIssue, { + githubNumber: issueExternal.number, + url: issueExternal.url, + repository: repo, + descriptionLocked: isDescriptionLocked + }) + await this.client.createMixin(issueId, github.mixin.GithubIssue, prj._id, prj.mixinClass, {}) + + await this.addConnectToMessage( + github.string.IssueConnectedActivityInfo, + prj._id, + issueId, + tracker.class.Issue, + issueExternal, + repository + ) + } + + async fillBackChanges (update: DocumentUpdate, existing: TGithubIssue, external: any): Promise {} + + async externalSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + kind: ExternalSyncField, + syncDocs: DocSyncInfo[], + repo: GithubIntegrationRepository, + prj: GithubProject + ): Promise { + // Wait global project sync + await integration.syncLock.get(prj._id) + + const ids = syncDocs.map((it) => (it.external as IssueExternalData).id).filter((it) => it !== undefined) + // + let partsize = 50 + try { + while (true) { + const idsPart = ids.splice(0, partsize) + if (idsPart.length === 0) { + break + } + const idsp = idsPart.map((it) => `"${it}"`).join(', ') + try { + const response: any = await this.ctx.with( + 'graphql.listIssue', + { prj: prj.name, repo: repo.name }, + async () => + await integration.octokit.graphql( + `query listIssues { + nodes(ids: [${idsp}] ) { + ... on Issue { + ${issueDetails(true)} + } + } + }` + ), + { + prj: prj.name, + repo: repo.name, + ids: idsp + } + ) + const issues: IssueExternalData[] = response.nodes + + if (issues.some((issue) => issue.url === undefined && Object.keys(issue).length === 0)) { + this.ctx.error('empty document content', { + repo: repo.name, + workspace: this.provider.getWorkspaceId().name, + data: cutObjectArray(response) + }) + } + + await this.syncIssues(tracker.class.Issue, repo, issues, derivedClient) + } catch (err: any) { + if (partsize > 1) { + partsize = 1 + ids.push(...idsPart) + this.ctx.warn('issue external retrieval switch to one by one mode', { + errors: err.errors, + msg: err.message, + workspace: this.provider.getWorkspaceId().name + }) + } else if (partsize === 1) { + // We need to update issue, since it is missing on external side. + const syncDoc = syncDocs.find((it) => it.external.id === idsPart[0]) + if (syncDoc !== undefined) { + this.ctx.warn('mark missing external PR', { + errors: err.errors, + msg: err.message, + url: syncDoc.url, + workspace: this.provider.getWorkspaceId().name + }) + await derivedClient.diffUpdate( + syncDoc, + { + needSync: githubSyncVersion, + externalVersion: githubExternalSyncVersion, + derivedVersion: githubDerivedSyncVersion + }, + Date.now() + ) + } + } + } + } + for (const d of syncDocs) { + if ((d.external as IssueExternalData).id == null) { + this.ctx.error('failed to do external sync for', { objectClass: d.objectClass, _id: d._id }) + // no external data for doc + await derivedClient.update(d, { + externalVersion: githubExternalSyncVersion + }) + } + } + this.provider.sync() + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error('Error', { err }) + } + } + + repositoryDisabled (integration: IntegrationContainer, repo: GithubIntegrationRepository): void { + integration.synchronized.delete(`${repo._id}:issues`) + } + + async externalFullSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + projects: GithubProject[], + repositories: GithubIntegrationRepository[] + ): Promise { + for (const repo of repositories) { + const prj = projects.find((it) => repo.githubProject === it._id) + if (prj === undefined) { + continue + } + // Wait global project sync + await integration.syncLock.get(prj._id) + + const syncKey = `${repo._id}:issues` + if ( + repo.githubProject === undefined || + !repo.enabled || + integration.synchronized.has(syncKey) || + integration.octokit === undefined + ) { + if (!repo.enabled) { + integration.synchronized.delete(syncKey) + } + continue + } + const since = await getSince(this.client, tracker.class.Issue, repo) + + this.ctx.info('sync external issues', { repo: repo.name, since, workspace: this.provider.getWorkspaceId().name }) + + const i = integration.octokit.graphql.paginate.iterator( + `query listIssue($name: String!, $owner: String!, $since: DateTime!, $cursor: String) { + repository(name: $name, owner: $owner) { + issues(first: 50, orderBy: {field: UPDATED_AT, direction: ASC}, filterBy: { since: $since }, after: $cursor) { + nodes { + ${issueDetails(true)} + } + pageInfo { + startCursor + hasNextPage + endCursor + } + totalCount + } + } + } + `, + { + name: repo.name, + owner: repo.owner?.login ?? '', + since: since ?? '1970-01-01T01:00:00Z' + } + ) + try { + for await (const data of i) { + const issues: IssueExternalData[] = data.repository.issues.nodes + if (issues.some((issue) => issue.url === undefined && Object.keys(issue).length === 0)) { + this.ctx.error('empty document content', { + repo: repo.name, + workspace: this.provider.getWorkspaceId().name, + data: cutObjectArray(data) + }) + } + await this.syncIssues(tracker.class.Issue, repo, issues, derivedClient) + this.provider.sync() + } + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + } + + this.ctx.info('sync external issues - done', { + repo: repo.name, + since, + workspace: this.provider.getWorkspaceId().name + }) + integration.synchronized.add(syncKey) + } + } +} diff --git a/services/github/pod-github/src/sync/projects.ts b/services/github/pod-github/src/sync/projects.ts new file mode 100644 index 0000000000..db5201e2c2 --- /dev/null +++ b/services/github/pod-github/src/sync/projects.ts @@ -0,0 +1,1150 @@ +import { Analytics } from '@hcengineering/analytics' +import { PersonAccount } from '@hcengineering/contact' +import core, { + AnyAttribute, + Class, + Data, + Doc, + DocumentUpdate, + EnumOf, + MeasureContext, + Ref, + TxOperations, + generateId +} from '@hcengineering/core' +import { getEmbeddedLabel, translate } from '@hcengineering/platform' +import { LiveQuery } from '@hcengineering/query' +import task from '@hcengineering/task' +import tracker, { Milestone } from '@hcengineering/tracker' +import { RepositoryEvent } from '@octokit/webhooks-types' +import github, { + DocSyncInfo, + GithubFieldMapping, + GithubIntegrationRepository, + GithubMilestone, + GithubProject, + GithubProjectSyncData +} from '@hcengineering/github' +import { deepEqual } from 'fast-equals' +import { Octokit } from 'octokit' +import { + ContainerFocus, + DocSyncManager, + ExternalSyncField, + IntegrationContainer, + IntegrationManager, + githubSyncVersion +} from '../types' +import { + GithubDataType, + GithubProjectV2, + GithubProjectV2Field, + GithubProjectV2ItemFieldValue, + IssueExternalData, + projectV2Field, + projectV2ItemFields, + supportedGithubTypes +} from './githubTypes' +import { syncConfig } from './syncConfig' +import { + collectUpdate, + errorToObj, + getPlatformType, + getType, + gqlp, + hashCode, + isGHWriteAllowed, + syncRunner +} from './utils' + +const githubColors = ['GRAY', 'BLUE', 'GREEN', 'YELLOW', 'ORANGE', 'RED', 'PINK', 'PURPLE'] + +const categoryColors = { + [task.statusCategory.UnStarted]: githubColors[0], + [task.statusCategory.ToDo]: githubColors[3], + [task.statusCategory.Active]: githubColors[1], + [task.statusCategory.Won]: githubColors[2], + [task.statusCategory.Lost]: githubColors[7] +} + +interface GithubMilestoneExternalData { + url: string + projectNumber: number + projectId: string + label: string + description: string + updatedAt: string +} + +interface MilestoneData { + label: string + description: string +} + +export class ProjectsSyncManager implements DocSyncManager { + provider!: IntegrationManager + + externalDerivedSync = false + + constructor ( + readonly ctx: MeasureContext, + readonly client: TxOperations, + readonly lq: LiveQuery + ) {} + + async init (provider: IntegrationManager): Promise { + this.provider = provider + } + + async sync ( + existing: Doc | undefined, + info: DocSyncInfo, + parent?: DocSyncInfo + ): Promise | undefined> { + const container = await this.provider.getContainer(info.space) + if (container?.container === undefined) { + return {} + } + + const okit = await this.provider.getOctokit(container.project.createdBy as Ref) + if (okit === undefined) { + this.ctx.info('No Authentication for author, waiting for authentication.', { + workspace: this.provider.getWorkspaceId().name + }) + return { needSync: githubSyncVersion, error: 'Need authentication for user' } + } + + let checkStructure = false + + if ( + existing !== undefined && + this.client.getHierarchy().isDerived(existing._class, tracker.class.Milestone) && + container.container.type === 'Organization' + ) { + // If no external project for milestone exists, let's create it. + const milestone = existing as Milestone + if (info.external === undefined) { + checkStructure = true + try { + await this.ctx.withLog( + 'Create Milestone projectV2', + {}, + async () => { + await this.createMilestone(container.container, container.project, okit, milestone, info) + }, + { label: milestone.label } + ) + } catch (err: any) { + this.ctx.error('failed create milestone', err) + return { needSync: githubSyncVersion, error: errorToObj(err) } + } + } + + if (checkStructure) { + const m = (await this.client.findOne(github.mixin.GithubMilestone, { + _id: milestone._id as Ref + })) as GithubMilestone + + let { projectStructure, wasUpdates } = await this.ctx.withLog( + 'update project structure', + {}, + async () => + await syncRunner.exec( + m._id, + async () => + await this.updateFieldMappings( + container.container, + container.project, + m, + container.project.mixinClass, + okit + ) + ), + { label: milestone.label } + ) + + // Retrieve updated field + if (wasUpdates) { + projectStructure = (await this.ctx.withLog( + 'update project structure(sync/second step)', + {}, + async () => await this.queryProjectStructure(container.container, m), + { + label: m.label + } + )) as GithubProjectV2 + } + container.container.projectStructure.set(m._id, projectStructure) + } + const milestoneExternal = info.external as GithubMilestoneExternalData + + const messageData: MilestoneData = { + label: milestoneExternal.label, + description: await this.provider.getMarkup(container.container, milestoneExternal.description) + } + + await this.handleDiffUpdateMilestone(existing, info, messageData, container, milestoneExternal) + + return { current: messageData, needSync: githubSyncVersion } + } + + return { needSync: githubSyncVersion } + } + + private async createMilestone ( + integration: IntegrationContainer, + project: GithubProject, + okit: Octokit, + milestone: Milestone, + info: DocSyncInfo | undefined + ): Promise { + if (integration.type !== 'Organization') { + return + } + const response = await this.createProjectV2(integration, okit, milestone.label) + + if (response !== undefined) { + const data: GithubMilestoneExternalData = { + projectId: response.projectNodeId, + projectNumber: response.projectNumber, + url: response.url, + label: milestone.label, + description: '', + updatedAt: new Date().toISOString() + } + + if (info !== undefined) { + info.external = data + } + + await this.client.createMixin( + milestone._id, + milestone._class, + milestone.space, + github.mixin.GithubMilestone, + { + mappings: [], + url: response.url, + projectNodeId: response.projectNodeId, + projectNumber: response.projectNumber, + githubProjectName: milestone.label + } + ) + + const derivedClient = new TxOperations(this.client, core.account.System, true) + + if (info !== undefined) { + await derivedClient.update(info, { + external: data, + needSync: '' + }) + } + } + } + + async handleDiffUpdateMilestone ( + existing: Doc, + info: DocSyncInfo, + issueData: MilestoneData, + container: ContainerFocus, + issueExternal: GithubMilestoneExternalData + ): Promise> { + const existingMilestone = existing as Milestone + const previousData: MilestoneData = info.current ?? ({} as unknown as MilestoneData) + + const update = collectUpdate(previousData, issueData, Object.keys(issueData)) + + const allAttributes = this.client.getHierarchy().getAllAttributes(tracker.class.Milestone) + const platformUpdate = collectUpdate(previousData, existingMilestone, Array.from(allAttributes.keys())) + + const okit = + (await this.provider.getOctokit(existing.modifiedBy as Ref)) ?? container.container.octokit + + // Remove current same values from update + for (const [k, v] of Object.entries(update)) { + if ((existingMilestone as any)[k] === v) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (update as any)[k] + } + } + + for (const [k, v] of Object.entries(update)) { + const pv = (platformUpdate as any)[k] + if (pv != null && pv !== v) { + // We have conflict of values. + this.ctx.error('conflict', { identifier: existingMilestone._id, k, v, pv }) + + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (update as any)[k] + continue + } + } + + if (container !== undefined && okit !== undefined) { + if (platformUpdate.label !== undefined || platformUpdate.description !== undefined) { + await this.updateProjectV2(okit, issueExternal.projectId, { + title: platformUpdate.label, + readme: + platformUpdate.description !== undefined + ? await this.provider.getMarkdown(platformUpdate.description) + : undefined + }) + } + } + + if (Object.keys(update).length > 0) { + // We have some fields to update of existing from external + await this.client.update(existingMilestone, update, false, new Date(issueExternal.updatedAt).getTime()) + } + + // We need to trigger external version retrieval, via sync or event, to prevent move sync operations from platform before we will be sure all is updated on github. + return { current: issueData, needSync: githubSyncVersion } + } + + async handleEvent(integration: IntegrationContainer, derivedClient: TxOperations, evt: T): Promise { + const event = evt as RepositoryEvent + + const { project, repository } = await this.provider.getProjectAndRepository(event.repository.node_id) + + if (project === undefined || repository === undefined) { + this.ctx.error('Unable to find project and repository for event', { + name: event.repository.name, + workspace: this.provider.getWorkspaceId().name + }) + return + } + + if (project !== undefined) { + const projectStructure = (await this.ctx.withLog( + 'update project structure(handleEvent)', + { prj: project.name }, + async () => await this.queryProjectStructure(integration, project) + )) as GithubProjectV2 + + integration.projectStructure.set(project._id, projectStructure) + } + } + + async handleDelete ( + existing: Doc | undefined, + info: DocSyncInfo, + derivedClient: TxOperations, + deleteExisting: boolean + ): Promise { + return false + } + + async externalSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + kind: ExternalSyncField, + syncDocs: DocSyncInfo[], + repository: GithubIntegrationRepository, + project: GithubProject + ): Promise {} + + repositoryDisabled (integration: IntegrationContainer, repo: GithubIntegrationRepository): void { + integration.synchronized.delete(`${repo._id}:issues`) + } + + async externalFullSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + projects: GithubProject[], + repositories: GithubIntegrationRepository[] + ): Promise { + for (const prj of projects) { + // Wait global project sync + await integration.syncLock.get(prj._id) + + const syncKey = `project_structure${prj._id}` + if ( + prj === undefined || + integration.synchronized.has(syncKey) || + integration.octokit === undefined || + integration.integration.createdBy === undefined + ) { + continue + } + + const okit = await this.provider.getOctokit(integration.integration.createdBy as Ref) + if (okit === undefined) { + this.ctx.info('No Authentication for author, waiting for authentication.', { + workspace: this.provider.getWorkspaceId().name + }) + continue + } + + // Check if project skill exists, on github + if (syncConfig.MainProject && integration.type === 'Organization') { + if (prj.projectNumber === undefined) { + try { + await this.ctx.withLog('Create projectV2', { prj: prj.name }, async () => { + const response = await this.createProjectV2(integration, okit, prj.name) + if (response !== undefined) { + prj.projectNumber = response.projectNumber + prj.projectNodeId = response.projectNodeId + + await this.client.update(prj, response) + } + }) + } catch (err: any) { + this.ctx.error('failed to create project', { prj: prj.name }) + continue + } + } + + try { + let { projectStructure, wasUpdates } = await this.ctx.withLog( + 'update project structure', + { prj: prj.name }, + async () => await this.updateFieldMappings(integration, prj, prj, prj.mixinClass, okit) + ) + + // Check if we have any changes in project, during our inactivity. + await this.ctx.withLog('check project v2 changes:', { prj: prj.name }, async () => { + await this.checkChanges(projectStructure, prj, prj._id, integration, derivedClient) + }) + + // Retrieve updated field + if (wasUpdates) { + projectStructure = (await this.ctx.withLog( + 'update project structure(second pass)', + { prj: prj.name }, + async () => await this.queryProjectStructure(integration, prj) + )) as GithubProjectV2 + } + + integration.projectStructure.set(prj._id, projectStructure) + } catch (err: any) { + this.ctx.error('filed to query project structure', err) + } + } + + if (syncConfig.SupportMilestones && integration.type === 'Organization') { + // Check project milestones and sync their structure as well. + const milestones = (await this.provider.liveQuery.queryFind(github.mixin.GithubMilestone, {})).filter( + (it) => it.space === prj._id + ) + for (const m of milestones) { + try { + let { projectStructure, wasUpdates } = await this.ctx.withLog( + 'update project structure', + { prj: m.label }, + async () => + await syncRunner.exec( + m._id, + async () => await this.updateFieldMappings(integration, prj, m, prj.mixinClass, okit) + ) + ) + + // Check if we have any changes in project, during our inactivity. + await this.ctx.withLog('check project v2 changes', { prj: prj.name }, async () => { + await this.checkChanges(projectStructure, m, prj._id, integration, derivedClient) + }) + + // Retrieve updated field + if (wasUpdates) { + projectStructure = (await this.ctx.withLog( + 'update project structure(second pass)', + { prj: prj.name }, + async () => await this.queryProjectStructure(integration, m) + )) as GithubProjectV2 + } + + integration.projectStructure.set(m._id, projectStructure) + } catch (err: any) { + this.ctx.error('filed to query project structure', err) + } + } + } + + integration.synchronized.add(syncKey) + } + } + + private async checkChanges ( + projectStructure: GithubProjectV2, + prj: GithubProject | GithubMilestone, + space: Ref, + integration: IntegrationContainer, + derivedClient: TxOperations + ): Promise { + if (projectStructure.projectV2.updatedAt !== prj.githubUpdatedAt) { + // ok, we need to synchronize all project items. + const { query, params, root } = this.queryProject(integration, prj) + const i = integration.octokit.graphql.paginate.iterator(query, params) + + // We need to collect a list of all uris of documents, and check if we have some missing ones. + const checkId = generateId() + try { + for await (const data of i) { + const items: { + id: string + type: string + updatedAt: string + fieldValues: { + nodes: GithubProjectV2ItemFieldValue[] + } + content: { + id: string + url: string + } + }[] = data[root].projectV2.items.nodes + + const syncInfos = await this.client.findAll(github.class.DocSyncInfo, { + space, + objectClass: tracker.class.Issue, + url: { $in: items.map((it) => (it.content.url ?? '').toLowerCase()) } + }) + + for (const item of items) { + let needSync = false + const itemSyncData = syncInfos.find((it) => it.url === item.content.url.toLocaleLowerCase()) + if (itemSyncData !== undefined) { + // We had item already, let's check our project field content and request update. + const external = itemSyncData.external as IssueExternalData + const dataIdx = external.projectItems.nodes.findIndex((it) => it.project.id === prj.projectNodeId) + if (dataIdx !== -1) { + const data = external.projectItems.nodes[dataIdx] + if (!deepEqual(data?.fieldValues.nodes, item.fieldValues.nodes)) { + // TODO: replace value + data.fieldValues = item.fieldValues + needSync = true + } + } else { + // No project information + needSync = true + } + // Mark all our existing sync documents, so we could find any missing ones. + await derivedClient.update( + itemSyncData, + needSync + ? { + external: itemSyncData.external, + externalCheckId: checkId, + needSync: '' + } + : { externalCheckId: checkId } + ) + } + } + + this.provider.sync() + } + } catch (err: any) { + this.ctx.error('filed in checkChanges', err) + Analytics.handleError(err) + } + + while (true) { + // Find all missing items + const missingInfos = await this.client.findAll( + github.class.DocSyncInfo, + { + space, + objectClass: tracker.class.Issue, + externalCheckId: { $ne: checkId }, + targetNodeId: prj.projectNodeId, + external: { $exists: true } // Skip not created items yet + }, + { limit: 50 } + ) + for (const u of missingInfos) { + // We need to sync + const udata = u.external as IssueExternalData + if (udata.projectItems !== undefined) { + udata.projectItems = { + nodes: (udata.projectItems?.nodes ?? []).filter((it) => it.project.id !== prj.projectNodeId) + } + } + await derivedClient.update(u, { needSync: '', external: u.external, externalCheckId: checkId }) + } + if (missingInfos.length === 0) { + break + } + } + this.provider.sync() + + await this.client.update(prj, { + githubUpdatedAt: projectStructure.projectV2.updatedAt + }) + } + } + + updateSet = new Map>() + + private async updateFieldMappings ( + integration: IntegrationContainer, + prj: GithubProject, + target: GithubProject | GithubMilestone, + mixinClass: Ref>, + okit: Octokit + ): Promise<{ projectStructure: GithubProjectV2, wasUpdates: boolean, mappings: GithubFieldMapping[] }> { + let projectStructure = await this.queryProjectStructure(integration, target) + let mappings = target.mappings + + if (projectStructure === undefined) { + if (this.client.getHierarchy().isDerived(tracker.class.Project, target._class)) { + // We need to re-create project. + const project = target as GithubProject + await this.ctx.withLog( + 'Create projectV2', + { name: 'name' in target ? target.name : target.label }, + async () => { + const response = await this.createProjectV2(integration, okit, project.name) + if (response !== undefined) { + target.projectNumber = response.projectNumber + target.projectNodeId = response.projectNodeId + } + + mappings = [] + await this.client.update(target, { ...response, mappings: [] }) + } + ) + } else { + const milestone = target as GithubMilestone + try { + await this.ctx.withLog('Create Milestone projectV2', { label: milestone.label }, async () => { + await this.createMilestone(integration, prj, okit, milestone, undefined) + mappings = [] + }) + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error('Error', { err }) + } + } + projectStructure = (await this.queryProjectStructure(integration, target)) as GithubProjectV2 + } + + const h = this.client.getHierarchy() + const allFields = h.getOwnAttributes(mixinClass) + + const githubFields: GithubProjectV2Field[] = projectStructure.projectV2.fields.edges + + const mHash = JSON.stringify(mappings) + // Create any platform field into matching github field + for (const [, f] of allFields.entries()) { + const existingField = (mappings ?? []).find((it) => it._id === f._id) + if (f.hidden === true) { + continue + } + if (f.isCustom === true && existingField === undefined) { + await this.createUpdateSimpleAttribute(f, githubFields, okit, target, mappings) + } + } + const statusF = h.getAttribute(tracker.class.Issue, 'status') + const f = await this.createUpdateStatus(githubFields, statusF, okit, target, prj) + if (f !== undefined) { + await this.pushMapping(target, mappings, statusF, f) + } + const priorityF = h.getAttribute(tracker.class.Issue, 'priority') + const pf = await this.createUpdatePriority(githubFields, priorityF, okit, target) + if (pf !== undefined) { + await this.pushMapping(target, mappings, priorityF, pf) + } + + await this.createUpdateSimpleAttribute( + h.getAttribute(tracker.class.Issue, 'estimation'), + githubFields, + okit, + target, + mappings + ) + + await this.createUpdateSimpleAttribute( + h.getAttribute(tracker.class.Issue, 'reportedTime'), + githubFields, + okit, + target, + mappings + ) + + await this.createUpdateSimpleAttribute( + h.getAttribute(tracker.class.Issue, 'remainingTime'), + githubFields, + okit, + target, + mappings + ) + + for (const fieldNode of githubFields) { + const existingField = (target.mappings ?? []).find((it) => it.githubId === fieldNode.node.id) + if (existingField !== undefined) { + continue + } + + if (supportedGithubTypes.has(fieldNode.node.dataType)) { + // try to find existing attribute + let matchedField: AnyAttribute | undefined + for (const [k, attr] of allFields) { + if (attr.type._class !== getPlatformType(fieldNode.node.dataType)) { + // Skip non matched fields. + continue + } + if (k.toLowerCase() === fieldNode.node.name.toLowerCase()) { + matchedField = attr + break + } + const labelValue = await translate(attr.label, {}) + if (labelValue.toLowerCase() === fieldNode.node.name.toLowerCase()) { + matchedField = attr + break + } + } + + if (matchedField !== undefined) { + // Ok we have field matched. + await this.pushMapping( + prj, + mappings, + { _id: matchedField._id, name: matchedField.name, attributeOf: matchedField.attributeOf }, + fieldNode + ) + continue + } + + if (fieldNode.node.dataType === 'SINGLE_SELECT') { + // TODO: Add enum update's + await this.createEnumAttribute(fieldNode, target, mappings, mixinClass) + } else if (fieldNode.node.dataType === 'NUMBER') { + await this.createSimpleAttribute(fieldNode, target, mappings, '0', mixinClass) + } else if (fieldNode.node.dataType === 'TEXT') { + await this.createSimpleAttribute(fieldNode, target, mappings, '', mixinClass) + } else if (fieldNode.node.dataType === 'DATE') { + await this.createSimpleAttribute(fieldNode, target, mappings, '', mixinClass) + } else if (fieldNode.node.dataType === 'ITERATION') { + // TODO: Handle Iteration data type. + } + } + } + return { projectStructure, wasUpdates: mHash !== JSON.stringify(target.mappings), mappings } + } + + private async createUpdateSimpleAttribute ( + field: AnyAttribute, + githubFields: GithubProjectV2Field[], + okit: Octokit, + target: GithubProject | GithubMilestone, + mappings: GithubFieldMapping[] + ): Promise { + const v = await this.createUpdateCustomField(githubFields, field, okit, target) + if (v !== undefined) { + await this.pushMapping(target, mappings, field, v) + } + } + + private async createEnumAttribute ( + fieldNode: GithubProjectV2Field, + prj: GithubProject | GithubMilestone, + mappings: GithubFieldMapping[], + mixinClass: Ref> + ): Promise { + const enumValues = (fieldNode.node.options ?? []).map((it) => it.name) + const enumId = await this.client.createDoc(core.class.Enum, core.space.Model, { + name: `Github_${'name' in prj ? prj.name : prj.label}_${fieldNode.node.name}`, + enumValues + }) + const enumType: EnumOf = { + _class: core.class.EnumOf, + of: enumId, + label: getEmbeddedLabel(fieldNode.node.name) + } + const data: Data = { + attributeOf: mixinClass, + name: fieldNode.node.id, + label: getEmbeddedLabel(fieldNode.node.name), + isCustom: true, + type: enumType, + defaultValue: enumValues[0] + } + // Create new attribute + const attrId = await this.client.createDoc( + core.class.Attribute, + core.space.Model, + data, + undefined, + Date.now(), + prj.createdBy + ) + await this.pushMapping(prj, mappings, { _id: attrId, name: data.name, attributeOf: data.attributeOf }, fieldNode) + } + + private async createSimpleAttribute ( + fieldNode: GithubProjectV2Field, + prj: GithubProject | GithubMilestone, + mappings: GithubFieldMapping[], + defaultValue: string, + mixinClass: Ref> + ): Promise { + const data: Data = { + attributeOf: mixinClass, + name: fieldNode.node.id, // Use github field id as name + label: getEmbeddedLabel(fieldNode.node.name), + isCustom: true, + type: { + _class: getPlatformType(fieldNode.node.dataType), + label: getEmbeddedLabel(fieldNode.node.name) + }, + defaultValue + } + // Create new attribute + const attrId = await this.client.createDoc( + core.class.Attribute, + core.space.Model, + data, + undefined, + Date.now(), + prj.createdBy + ) + await this.pushMapping(prj, mappings, { _id: attrId, name: data.name, attributeOf: data.attributeOf }, fieldNode) + } + + private async pushMapping ( + prj: GithubProject | GithubMilestone, + mappings: GithubFieldMapping[], + f: Pick, + node: GithubProjectV2Field + ): Promise { + const field = mappings.find((it) => it._id === f._id) + if (field !== undefined) { + return + } + const m = { + _id: f._id, + name: f.name, + _class: f.attributeOf, + githubId: node.node.id + } + mappings.push(m) + await this.client.update(prj, { + $push: { + mappings: m + } + }) + } + + private async createUpdateStatus ( + githubFields: GithubProjectV2Field[], + statusAttr: AnyAttribute, + okit: Octokit, + prj: GithubProject | GithubMilestone, + project: GithubProject + ): Promise { + // TODO: A support of field upgrade + const githubAttr = githubFields + .map((it) => it) + .find( + (it) => + it.node.name.toLowerCase() === 'uber' + statusAttr.name.toLowerCase() && it.node.dataType === 'SINGLE_SELECT' + ) + if (githubAttr !== undefined) { + return githubAttr + } + + // Let's find all platform status fields + const statusFields = await this.provider.getProjectStatuses(project.type) + + const opts: { name: string, color: string, description: string }[] = [] + for (const fi of statusFields) { + opts.push({ + name: fi.name, + description: fi.description ?? '', + color: categoryColors[fi.category ?? task.statusCategory.UnStarted] + }) + } + + if (isGHWriteAllowed()) { + const fieldUpdateResponse: any = await okit.graphql( + `mutation createProjectField { + ${this.addProjectField( + prj.projectNodeId as string, + 'Uber' + statusAttr.name[0].toUpperCase() + statusAttr.name.slice(1), + 'SINGLE_SELECT', + opts + )} + } + ` + ) + return { node: fieldUpdateResponse.createProjectV2Field.projectV2Field as GithubProjectV2Field['node'] } + } + } + + private async createUpdateCustomField ( + githubFields: GithubProjectV2Field[], + attr: AnyAttribute, + okit: Octokit, + prj: GithubProject | GithubMilestone + ): Promise { + const attrType = getType(attr) + if (attrType === undefined) { + return undefined + } + // TODO: A support of field upgrade + let githubAttr = githubFields + .map((it) => it) + .find((it) => it.node.name.toLowerCase() === attr.name.toLowerCase() && it.node.dataType === attrType) + if (githubAttr !== undefined) { + return githubAttr + } + + // Find using label + + const labelValue = await translate(attr.label, {}) + + githubAttr = githubFields + .map((it) => it) + .find((it) => it.node.name.toLowerCase() === labelValue.toLowerCase() && it.node.dataType === attrType) + + if (githubAttr !== undefined) { + return githubAttr + } + + let opts: { name: string, color: string, description: string }[] | undefined + + if (attrType === 'SINGLE_SELECT') { + const typeOf = attr.type as EnumOf + const enumClass = await this.client.findOne(core.class.Enum, { _id: typeOf.of }) + opts = [] + for (const fi of enumClass?.enumValues ?? []) { + opts.push({ + name: fi, + description: '', + color: githubColors[Math.abs(hashCode(fi)) % githubColors.length] + }) + } + } + + if (isGHWriteAllowed()) { + const fieldUpdateResponse: any = await okit.graphql( + `mutation createProjectField { + ${this.addProjectField( + prj.projectNodeId as string, + labelValue[0].toUpperCase() + labelValue.slice(1), + attrType, + opts + )} + } + ` + ) + return { node: fieldUpdateResponse.createProjectV2Field.projectV2Field as GithubProjectV2Field['node'] } + } + } + + private async createUpdatePriority ( + githubFields: GithubProjectV2Field[], + attr: AnyAttribute, + okit: Octokit, + prj: GithubProject | GithubMilestone + ): Promise { + // TODO: A support of field upgrade + const githubAttr = githubFields + .map((it) => it) + .find((it) => it.node.name.toLowerCase() === attr.name.toLowerCase() && it.node.dataType === 'SINGLE_SELECT') + if (githubAttr !== undefined) { + return githubAttr + } + + const opts: { name: string, color: string, description: string }[] = [] + + for (const fi of ['Urgent', 'High', 'Medium', 'Low']) { + opts.push({ + name: fi, + description: '', + color: githubColors[Math.abs(hashCode(fi)) % githubColors.length] + }) + } + if (isGHWriteAllowed()) { + const fieldUpdateResponse: any = await okit.graphql( + `mutation createProjectField { + ${this.addProjectField( + prj.projectNodeId as string, + attr.name[0].toUpperCase() + attr.name.slice(1), + 'SINGLE_SELECT', + opts + )} + } + ` + ) + return { node: fieldUpdateResponse.createProjectV2Field.projectV2Field as GithubProjectV2Field['node'] } + } + } + + private async queryProjectStructure ( + integration: IntegrationContainer, + prj: GithubProjectSyncData + ): Promise { + const root = `${integration.type === 'Organization' ? 'organization' : 'user'}` + return ( + (await integration.octokit?.graphql( + ` + query projectStructureQuery($login: String!, $prjNumber: Int!) { + ${root}(login: $login) { + projectV2(number: $prjNumber) { + id + updatedAt + title + readme + fields(last: 100) { + edges { + node { + ${projectV2Field} + } + } + } + } + } + }`, + { + login: integration.login, + prjNumber: prj.projectNumber + } + )) as any + )[root] + } + + private queryProject ( + integration: IntegrationContainer, + prj: GithubProjectSyncData + ): { query: string, params: any, root: string } { + const root = `${integration.type === 'Organization' ? 'organization' : 'user'}` + return { + query: ` + query queryProjectContents($login: String!, $prjNumber: Int!, $cursor: String) { + ${root}(login: $login) { + projectV2(number: $prjNumber) { + items(first: 99, after: $cursor) { + nodes { + id + type + updatedAt + fieldValues(first: 50) { + nodes { + ${projectV2ItemFields} + } + } + content { + ... on Issue { + id + url + } + ... on PullRequest { + id + url + } + } + } + pageInfo { + startCursor + hasNextPage + endCursor + } + totalCount + } + } + } + } + `, + params: { + login: integration.login, + prjNumber: prj.projectNumber + }, + root + } + } + + private async createProjectV2 ( + integration: IntegrationContainer, + octokit: Octokit, + prjName: string + ): Promise<{ projectNumber: number, projectNodeId: string, url: string } | undefined> { + if (isGHWriteAllowed()) { + const response: any = await octokit.graphql( + ` + mutation createProjectV2($owner: ID!, $title: String!) { + createProjectV2(input: {ownerId: $owner, title: $title}) { + projectV2 { + url + id + number + } + } + }`, + { + owner: integration.loginNodeId, + title: prjName + } + ) + + return { + projectNumber: response.createProjectV2.projectV2.number, + projectNodeId: response.createProjectV2.projectV2.id, + url: response.createProjectV2.projectV2.url + } + } + } + + private async updateProjectV2 ( + octokit: Octokit, + projectId: string, + options: { + title?: string + shortDescription?: string + readme?: string + } + ): Promise { + if (isGHWriteAllowed()) { + await octokit.graphql( + ` + mutation createProjectV2($projectID: ID!) { + updateProjectV2(input: { + projectId: $projectID + ${gqlp(options)} + }) { + projectV2 { + url + id + number + } + } + }`, + { + projectID: projectId + } + ) + } + } + + private addProjectField ( + projectId: string, + name: string, + type: GithubDataType, + options?: { + name: string + color: string + description: string + }[] + ): string { + return ` + createProjectV2Field( + input: { + dataType: ${type}, + name: "${name}", + ${ + options !== undefined + ? `singleSelectOptions: [ + ${options + .map((it) => `{name: "${it.name}", color: ${it.color}, description: "${it.description}"}`) + .join(', ')}], ` + : '' + } + projectId: "${projectId}" + } + ) { + projectV2Field { + ${projectV2Field} + } + } + \n` + } +} diff --git a/services/github/pod-github/src/sync/pullrequests.ts b/services/github/pod-github/src/sync/pullrequests.ts new file mode 100644 index 0000000000..c1c288005f --- /dev/null +++ b/services/github/pod-github/src/sync/pullrequests.ts @@ -0,0 +1,1539 @@ +import { Analytics } from '@hcengineering/analytics' +import { Person, PersonAccount } from '@hcengineering/contact' +import core, { + Account, + AttachedData, + Doc, + DocumentUpdate, + Ref, + SortingOrder, + Status, + TxCUD, + TxCollectionCUD, + TxMixin, + TxOperations, + TxProcessor, + WithLookup, + cutObjectArray, + generateId, + getCollaborativeDoc, + getCollaborativeDocId +} from '@hcengineering/core' +import task, { TaskType, calcRank, makeRank } from '@hcengineering/task' +import { isEmptyMarkup } from '@hcengineering/text' +import time, { ToDo, ToDoPriority } from '@hcengineering/time' +import tracker, { Issue, IssuePriority, IssueStatus, Project } from '@hcengineering/tracker' +import { OctokitResponse } from '@octokit/types' +import { ProjectsV2ItemEvent, PullRequestEvent } from '@octokit/webhooks-types' +import github, { + DocSyncInfo, + GithubIntegrationRepository, + GithubIssue, + GithubIssueStateReason, + GithubProject, + GithubPullRequest, + GithubPullRequestState, + GithubTodo, + LastReviewState +} from '@hcengineering/github' +import { Octokit } from 'octokit' +import config from '../config' +import { + ContainerFocus, + DocSyncManager, + ExternalSyncField, + IntegrationContainer, + UserInfo, + githubDerivedSyncVersion, + githubExternalSyncVersion, + githubSyncVersion +} from '../types' +import { + IssueExternalData, + PullRequestExternalData, + PullRequestReviewState, + Review, + getUpdatedAtReviewThread, + pullRequestDetails, + toPRState, + toReviewDecision, + toReviewState +} from './githubTypes' +import { GithubIssueData, IssueSyncManagerBase, IssueSyncTarget } from './issueBase' +import { syncConfig } from './syncConfig' +import { errorToObj, getSinceRaw, gqlp, guessStatus, isGHWriteAllowed, syncDerivedDocuments, syncRunner } from './utils' + +type GithubPullRequestData = GithubIssueData & +Omit + +export class PullRequestSyncManager extends IssueSyncManagerBase implements DocSyncManager { + externalDerivedSync = true + async handleEvent(integration: IntegrationContainer, derivedClient: TxOperations, evt: T): Promise { + const _event = evt as PullRequestEvent | ProjectsV2ItemEvent + + if (_event.sender.type === 'Bot') { + // Ignore events from Bot if it is our bot + // No need to handle event from ourself + if (_event.sender.login.includes(config.BotName)) { + return + } + } + this.ctx.info('pull request:handleEvent', { + nodeId: + (_event as PullRequestEvent).pull_request?.html_url ?? + (_event as ProjectsV2ItemEvent).projects_v2_item?.node_id, + action: _event.action, + login: _event.sender.login, + type: _event.sender.type, + workspace: this.provider.getWorkspaceId().name + }) + + const projectV2Event = (_event as any as ProjectsV2ItemEvent).projects_v2_item?.id !== undefined + if (projectV2Event) { + const projectV2Event = _event as ProjectsV2ItemEvent + + const githubProjects = await this.provider.liveQuery.queryFind(github.mixin.GithubProject, {}) + let prj = githubProjects.find((it) => it.projectNodeId === projectV2Event.projects_v2_item.project_node_id) + if (prj === undefined) { + // Checking for milestones + const m = (await this.provider.liveQuery.queryFind(github.mixin.GithubMilestone, {})).find( + (it) => it.projectNodeId === projectV2Event.projects_v2_item.project_node_id + ) + if (m !== undefined) { + prj = githubProjects.find((it) => it._id === m.space) + } + } + + if (prj === undefined) { + this.ctx.info('Event from unknown v2 project', { + nodeId: projectV2Event.projects_v2_item.project_node_id, + workspace: this.provider.getWorkspaceId().name + }) + return + } + + const urlId = projectV2Event.projects_v2_item.node_id + + await syncRunner.exec(urlId, async () => { + await this.processProjectV2Event(integration, projectV2Event, derivedClient, prj as GithubProject) + }) + } else { + const event = _event as PullRequestEvent + const { project, repository } = await this.provider.getProjectAndRepository(event.repository.node_id) + + if (project === undefined || repository === undefined) { + this.ctx.info('No project for repository', { + name: event.repository.name, + workspace: this.provider.getWorkspaceId().name + }) + return + } + const url = event.pull_request.issue_url + + await syncRunner.exec(url, async () => { + await this.processEvent(event, derivedClient, repository, integration, project) + }) + } + } + + private async processEvent ( + event: PullRequestEvent, + derivedClient: TxOperations, + repo: GithubIntegrationRepository, + integration: IntegrationContainer, + prj: GithubProject + ): Promise { + const account = (await this.provider.getAccountU(event.sender))?._id ?? core.account.System + + let externalData: PullRequestExternalData + try { + const response: any = await integration.octokit?.graphql( + `query listIssue($name: String!, $owner: String!, $issue: Int!) { + repository(name: $name, owner: $owner) { + pullRequest(number: $issue) { + ${pullRequestDetails} + } + } + } + `, + { + name: repo.name, + owner: repo.owner?.login, + issue: event.pull_request.number + } + ) + externalData = response.repository.pullRequest + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + await this.createErrorSyncDataByUrl( + event.pull_request.html_url, + event.pull_request.number, + new Date(event.pull_request.updated_at), + derivedClient, + repo, + err + ) + return + } + if (externalData === undefined) { + await this.createErrorSyncDataByUrl( + event.pull_request.html_url, + event.pull_request.number, + new Date(event.pull_request.updated_at), + derivedClient, + repo, + 'no external data found' + ) + return + } + + switch (event.action) { + case 'opened': { + await this.createSyncData(externalData, derivedClient, repo, account) + break + } + case 'edited': { + const update: DocumentUpdate = {} + const du: DocumentUpdate = {} + if (event.changes.title !== undefined) { + update.title = event.pull_request.title + } + if (event.changes.body !== undefined) { + update.description = await this.provider.getMarkup(integration, event.pull_request.body, this.stripGuestLink) + du.markdown = await this.provider.getMarkdown(update.description) + } + if (event.changes.base !== undefined) { + update.base = externalData.baseRef + } + await this.handleUpdate(externalData, derivedClient, update, account, prj, false, undefined, undefined, du) + break + } + case 'review_requested': { + const update: DocumentUpdate = {} + await this.handleUpdate(externalData, derivedClient, update, account, prj, true) + break + } + case 'review_request_removed': { + const update: DocumentUpdate = {} + await this.handleUpdate(externalData, derivedClient, update, account, prj, true) + break + } + case 'converted_to_draft': + case 'ready_for_review': { + await this.handleUpdate(externalData, derivedClient, {}, account, prj, true) + break + } + case 'assigned': + case 'unassigned': { + const assignees = await this.getAssignees(externalData) + const update: DocumentUpdate = { + assignee: assignees?.[0]?.person ?? null + } + await this.handleUpdate(externalData, derivedClient, update, account, prj, true) + break + } + case 'closed': + case 'reopened': { + const type = await this.provider.getTaskTypeOf(prj.type, github.class.GithubPullRequest) + const statuses = await this.provider.getStatuses(type?._id) + + const isMerged = event.pull_request?.merged_at !== null + + const update: DocumentUpdate = { + draft: externalData.isDraft, + head: externalData.headRef, + base: externalData.baseRef, + mergeable: externalData.mergeable, + commits: externalData.commits?.nodes?.length, + remainingTime: 0, + state: toPRState(externalData.state ?? 'OPEN'), + reviewDecision: toReviewDecision(externalData.reviewDecision ?? 'REVIEW_REQUIRED'), + ...(event.action === 'closed' + ? { + status: ( + await guessStatus( + { + state: 'CLOSED', + stateReason: isMerged ? GithubIssueStateReason.Completed : GithubIssueStateReason.NotPlanned + }, + statuses + ) + )._id, + mergedAt: + event.pull_request?.merged_at !== null ? new Date(event.pull_request?.merged_at).getTime() : null, + closedAt: + event.pull_request?.closed_at !== null ? new Date(event.pull_request?.closed_at).getTime() : null + } + : { + status: (await guessStatus({ state: 'OPEN', stateReason: GithubIssueStateReason.Reopened }, statuses)) + ._id, + mergedAt: null, + closedAt: null + }) + } + await this.handleUpdate( + externalData, + derivedClient, + update, + account, + prj, + true, + undefined, + async (state, existing, external, update) => { + // We need to be sure we not change status if category is same, since github doesn't know about it. + const existingStatus = statuses.find((it) => it._id === existing.status) + const updateState = statuses.find((it) => it._id === update.status) + if (existingStatus?.category === updateState?.category) { + delete update.status + } + return true + } + ) + break + } + case 'synchronize': { + const syncData = await this.client.findOne(github.class.DocSyncInfo, { + url: (externalData.url ?? '').toLowerCase() + }) + if (syncData !== undefined) { + await derivedClient.update(syncData, { + needSync: '', + external: externalData, + derivedVersion: '', // Check derived changes + updatePatch: true + }) + this.provider.sync() + } + break + } + } + } + + async getReviewers (issue: PullRequestExternalData): Promise { + // Find Assignees and reviewers + const ids: UserInfo[] = issue.reviewRequests.nodes.map((it: any) => it.requestedReviewer) + + const values: PersonAccount[] = [] + + for (const o of ids) { + const acc = await this.provider.getAccount(o) + if (acc !== undefined) { + values.push(acc) + } + } + + for (const n of issue.latestReviews.nodes) { + const acc = await this.provider.getAccount(n.author) + if (acc !== undefined) { + values.push(acc) + } + } + return values + } + + private async createSyncData ( + pullRequestExternal: PullRequestExternalData, + derivedClient: TxOperations | undefined, + repo: GithubIntegrationRepository, + account: Ref + ): Promise { + const lastModified = new Date(pullRequestExternal.updatedAt).getTime() + await derivedClient?.createDoc(github.class.DocSyncInfo, repo.githubProject as Ref, { + url: pullRequestExternal.url.toLowerCase(), + needSync: '', // we need to sync to retrieve patch in background + githubNumber: pullRequestExternal.number, + repository: repo._id, + objectClass: github.class.GithubPullRequest, + external: pullRequestExternal, + externalVersion: githubExternalSyncVersion, + derivedVersion: '', + allowOpenInHuly: true, + lastModified, + lastGithubUser: account + }) + // We need trigger comments, if their sync data created before + const childInfos = await this.client.findAll(github.class.DocSyncInfo, { + parent: (pullRequestExternal.url ?? '').toLowerCase() + }) + for (const child of childInfos) { + await derivedClient?.update(child, { needSync: '' }) + } + this.provider.sync() + } + + async syncToTarget ( + target: IssueSyncTarget, + container: ContainerFocus, + existing: Doc | undefined, + pullRequestExternal: PullRequestExternalData, + derivedClient: TxOperations, + info: DocSyncInfo + ): Promise> { + const account = + existing?.modifiedBy ?? (await this.provider.getAccount(pullRequestExternal.author))?._id ?? core.account.System + const accountGH = + info.lastGithubUser ?? (await this.provider.getAccount(pullRequestExternal.author))?._id ?? core.account.System + + // A target node id + const targetNodeId: string | undefined = info.targetNodeId as string + + const okit = (await this.provider.getOctokit(account as Ref)) ?? container.container.octokit + + const isProjectProjectTarget = target.target.projectNodeId === target.project.projectNodeId + const supportProjects = + (isProjectProjectTarget && syncConfig.MainProject) || (!isProjectProjectTarget && syncConfig.SupportMilestones) + + const type = await this.provider.getTaskTypeOf(container.project.type, github.class.GithubPullRequest) + const statuses = await this.provider.getStatuses(type?._id) + + if ( + targetNodeId !== undefined && + target.target.projectNodeId !== undefined && + targetNodeId !== target.target.projectNodeId && + supportProjects + ) { + const itemNode = pullRequestExternal.projectItems.nodes.find((it) => it.project.id === targetNodeId) + if (itemNode !== undefined) { + await this.removeIssueFromProject(okit, target.target.projectNodeId, itemNode.id) + // remove data + pullRequestExternal.projectItems.nodes = pullRequestExternal.projectItems.nodes.filter( + (it) => it.id !== targetNodeId + ) + await derivedClient.update(info, { + external: pullRequestExternal, + externalVersion: githubExternalSyncVersion + }) + target.prjData = undefined + // We need to sync from platform as new to new project. + // We need to remove current sync + info.current = {} + } + } + + // Check if issue are added to project. + if (target.prjData === undefined && okit !== undefined && supportProjects) { + try { + target.prjData = await this.ctx.withLog( + 'add pull request to project}', + {}, + async () => + await this.addIssueToProject(container, okit, pullRequestExternal, target.target.projectNodeId as string), + { url: pullRequestExternal.url } + ) + if (target.prjData !== undefined) { + pullRequestExternal.projectItems.nodes.push(target.prjData) + } + + await derivedClient.update(info, { + external: pullRequestExternal, + externalVersion: githubExternalSyncVersion + }) + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + return { needSync: githubSyncVersion, error: errorToObj(err) } + } + } + + const assignees = await this.getAssignees(pullRequestExternal) + const reviewers = await this.getReviewers(pullRequestExternal) + + const latestReviews: LastReviewState[] = [] + + for (const d of pullRequestExternal.latestReviews?.nodes ?? []) { + const author = (await this.provider.getAccount(d.author))?._id + if (author !== undefined) { + latestReviews.push({ + state: toReviewState(d.state), + user: author + }) + } + } + const pullRequestData: GithubPullRequestData = { + title: pullRequestExternal.title, + description: await this.provider.getMarkup(container.container, pullRequestExternal.body, this.stripGuestLink), + assignee: assignees[0]?.person ?? null, + reviewers: reviewers.map((it) => it.person), + draft: pullRequestExternal.isDraft, + head: pullRequestExternal.headRef, + base: pullRequestExternal.baseRef, + mergedAt: pullRequestExternal.mergedAt != null ? new Date(pullRequestExternal.mergedAt).getTime() : null, + closedAt: pullRequestExternal.closedAt != null ? new Date(pullRequestExternal.closedAt).getTime() : null, + mergeable: pullRequestExternal.mergeable, + commits: pullRequestExternal.commits?.nodes?.length, + remainingTime: 0, + state: toPRState(pullRequestExternal.state ?? 'OPEN'), + latestReviews, + reviewDecision: toReviewDecision(pullRequestExternal.reviewDecision ?? 'REVIEW_REQUIRED'), + files: pullRequestExternal.files.totalCount + } + + const taskTypes = (await this.client.findAll(task.class.TaskType, { parent: container.project.type })).filter( + (it) => this.client.getHierarchy().isDerived(it.targetClass, github.class.GithubPullRequest) + ) + + if (taskTypes.length === 0) { + // Missing required task type + this.ctx.error('Missing required task type', { url: pullRequestExternal.url }) + return { needSync: githubSyncVersion } + } + await this.fillProjectV2Fields(target, container, pullRequestData, taskTypes[0]) + + const lastModified = new Date(pullRequestExternal.updatedAt).getTime() + + if (existing === undefined) { + try { + await this.ctx.withLog( + 'retrieve pull request patch', + {}, + async () => + await this.handlePatch( + info, + container, + pullRequestExternal, + { + _id: info._id as unknown as Ref, + space: info.space as Ref, + _class: github.class.GithubPullRequest + }, + lastModified, + accountGH + ), + { url: pullRequestExternal.url } + ) + const { markdownCompatible, markdown } = await this.provider.checkMarkdownConversion( + container.container, + pullRequestExternal.body + ) + await this.ctx.withLog( + 'create pull request in platform', + {}, + async () => { + await this.createPullRequest( + info, + accountGH, + { + ...pullRequestData, + status: (await guessStatus(pullRequestExternal, statuses))._id as Ref + }, + pullRequestExternal, + info.repository as Ref, + container.project, + taskTypes[0]._id, + container.repository.find((it) => it._id === info.repository) as GithubIntegrationRepository, + !markdownCompatible + ) + }, + { url: pullRequestExternal.url } + ) + + const pullRequestObj = await this.client.findOne(github.class.GithubPullRequest, { + _id: info._id as unknown as Ref + }) + if (pullRequestObj !== undefined) { + await this.todoSync(pullRequestObj, pullRequestExternal, info, account) + } + + return { + needSync: '', + external: pullRequestExternal, + externalVersion: githubExternalSyncVersion, + lastModified: new Date(pullRequestExternal.updatedAt).getTime(), + isDescriptionLocked: !markdownCompatible, + markdown + } + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + return { needSync: githubSyncVersion, error: errorToObj(err) } + } + } else { + try { + if (info.updatePatch === true) { + await this.ctx.withLog( + 'update pull request patch', + {}, + async () => + await this.handlePatch( + info, + container, + pullRequestExternal, + { + _id: info._id as unknown as Ref, + space: info.space as Ref, + _class: github.class.GithubPullRequest + }, + lastModified, + accountGH + ), + { url: pullRequestExternal.url } + ) + } + + const description = await this.ctx.withLog( + 'query collaborative pull request description', + {}, + async () => { + const collaborativeDoc = getCollaborativeDoc(getCollaborativeDocId(existing._id, 'description')) + const content = await this.collaborator.getContent(collaborativeDoc, 'description') + return isEmptyMarkup(content) ? (existing as Issue).description : content + }, + { url: pullRequestExternal.url } + ) + + const update = await this.ctx.withLog( + 'perform pull request diff update', + {}, + async () => + await this.handleDiffUpdate( + target, + { ...(existing as any), description }, + info, + pullRequestData, + container, + pullRequestExternal, + account, + accountGH, + supportProjects + ), + { url: pullRequestExternal.url } + ) + return { + ...update, + updatePatch: false, + lastModified: new Date(pullRequestExternal.updatedAt).getTime(), + lastGithubAccount: null + } + } catch (err: any) { + this.ctx.error('Error update pr', { err }) + Analytics.handleError(err) + return { needSync: githubSyncVersion, error: errorToObj(err), external: pullRequestExternal } + } + } + } + + async afterSync (existing: Issue, account: Ref, issueExternal: any, info: DocSyncInfo): Promise { + const pullRequest = existing as GithubPullRequest + await this.todoSync(pullRequest, issueExternal as PullRequestExternalData, info, account) + } + + async todoSync ( + pullRequest: Pick< + GithubPullRequest, + '_id' | 'identifier' | 'reviewers' | 'title' | 'state' | 'space' | '_class' | 'modifiedBy' + >, + external: PullRequestExternalData, + info: DocSyncInfo, + account: Ref + ): Promise { + // Find all todo's related to PR. + const allTodos = await this.client.findAll(github.mixin.GithubTodo, { attachedTo: pullRequest._id }) + // We also need to track deleted Todos, + const removedTodos: GithubTodo[] = [] + + const removedTodoOps = await this.client.findAll>( + core.class.TxCollectionCUD, + { + objectId: pullRequest._id, + 'tx.objectClass': time.class.ProjectToDo, + 'tx.objectId': { $nin: allTodos.map((it) => it._id) } + }, + { sort: { modifiedOn: SortingOrder.Ascending } } + ) + + const todoIds = removedTodoOps.filter((it) => it.tx._class === core.class.TxCreateDoc).map((it) => it.tx.objectId) + + const mixinOps = await this.client.findAll>( + core.class.TxMixin, + { + objectId: { $in: todoIds } + }, + { sort: { modifiedOn: SortingOrder.Ascending } } + ) + + const groupedByTodo = new Map, TxCUD[]>() + + const h = this.client.getHierarchy() + + // We need to rebuild removed todos's if pressent. + for (const tx of removedTodoOps) { + const ops = groupedByTodo.get(tx.tx.objectId) + groupedByTodo.set(tx.tx.objectId, [...(ops ?? []), tx.tx]) + } + + for (const tx of mixinOps) { + const ops = groupedByTodo.get(tx.objectId) + groupedByTodo.set(tx.objectId, [...(ops ?? []), tx]) + } + + for (const [, txes] of groupedByTodo) { + const todo = TxProcessor.buildDoc2Doc(txes) + if (todo !== undefined && h.hasMixin(todo, github.mixin.GithubTodo)) { + removedTodos.push(h.as(todo, github.mixin.GithubTodo)) + } + } + + const pendingOrDismissed = new Map, PullRequestReviewState>() + + const approvedOrChangesRequested = new Map, PullRequestReviewState>() + const reviewStates = new Map, PullRequestReviewState[]>() + + const sortedReviews: (Review & { date: number })[] = external.reviews.nodes.map((it) => ({ + ...it, + date: new Date(it.updatedAt ?? it.submittedAt ?? it.createdAt).getTime() + })) + + for (const it of external.latestReviews.nodes) { + if (sortedReviews.some((qt) => it.id === qt.id)) { + continue + } + + sortedReviews.push({ ...it, date: new Date(it.updatedAt ?? it.submittedAt ?? it.createdAt).getTime() }) + } + + sortedReviews.sort((a, b) => b.date - a.date) + + for (const r of sortedReviews) { + const rp = await this.provider.getAccount(r.author) + if (rp === undefined) { + continue + } + if (r.state === 'PENDING' || r.state === 'DISMISSED') { + pendingOrDismissed.set(rp.person, r.state) + } + if (r.state === 'APPROVED' || r.state === 'CHANGES_REQUESTED') { + approvedOrChangesRequested.set(rp.person, r.state) + } + reviewStates.set(rp.person, [...(reviewStates.get(rp.person) ?? []), r.state]) + } + + for (const r of pullRequest.reviewers ?? []) { + // Find all related todos's + const todos = [...allTodos, ...removedTodos].filter((it) => it.user === r && it.purpose === 'review') + // We also need to check if user deleted, todo, in this case we need to remove review request. + + const hasPending = todos.some((it) => it.doneOn !== null) + + // Create review Todo, if missing. + if ( + pullRequest.state === GithubPullRequestState.open || + (!hasPending && pendingOrDismissed.get(r) !== undefined) + ) { + if (todos.length === 0) { + await this.requestReview(pullRequest, external, r, account) + } + } + } + + // Handle change requests. + // If we have change requests pending, we need to create Todo to resolve them to author or assigned person, to resolve them. + + const changeRequestPersons = new Set>() + const author = await this.provider.getAccount(external.author) + if (author !== undefined) { + changeRequestPersons.add(author.person) + } + for (const au of external.assignees.nodes ?? []) { + const u = await this.provider.getAccount(au) + if (u !== undefined) { + changeRequestPersons.add(u.person) + } + } + + // Check review threads and create todo to resolve them. + const requestedIds: Ref[] = [] + + let allResolved = true + for (const r of external.reviewThreads.nodes) { + if (!r.isResolved) { + allResolved = false + for (const c of Array.from(changeRequestPersons)) { + // We need to add Todo to resolve PR. + const todos = [...allTodos, ...removedTodos].filter((it) => it.user === c && it.purpose === 'fix') + if (todos.length === 0) { + requestedIds.push(c) + // We do not have todos's create one to solve issue. + await this.requestFix(pullRequest, external, c, account) + } + } + break + } + } + + // Handle change request + if (external.reviewThreads.nodes.length === 0) { + // If we have changes requested. + for (const [, sst] of approvedOrChangesRequested.entries()) { + if (sst === 'CHANGES_REQUESTED') { + // We have changes requested and not resolved yet. + for (const c of Array.from(changeRequestPersons)) { + const todos = [...allTodos, ...removedTodos].filter((it) => it.user === c && it.purpose === 'fix') + if (todos.length === 0 && !requestedIds.includes(c)) { + requestedIds.push(c) + // We do not have todos's create one to solve issue. + await this.requestFix(pullRequest, external, c, account) + } + } + } + } + } + + if (allResolved) { + // We need to complete or remove todo, in case all are resolved. + if (!Array.from(approvedOrChangesRequested.values()).includes('CHANGES_REQUESTED')) { + const todos = allTodos.filter((it) => it.purpose === 'fix') + for (const t of todos) { + await this.markDoneOrDeleteTodo(t) + } + } + } + + // In case of merged, reviewed, we need to close todos's + if (pullRequest.state !== GithubPullRequestState.open) { + for (const td of allTodos.filter((it) => it.doneOn == null)) { + await this.markDoneOrDeleteTodo(td) + } + } + } + + private async requestReview ( + pullRequest: Pick, + external: PullRequestExternalData, + todoUser: Ref, + account: Ref + ): Promise { + const latestTodo = await this.client.findOne( + time.class.ToDo, + { + user: todoUser, + doneOn: null + }, + { + sort: { rank: SortingOrder.Ascending } + } + ) + const todoId = await this.client.addCollection( + time.class.ProjectToDo, + time.space.ToDos, + pullRequest._id, + pullRequest._class, + 'todos', + { + title: 'Review ' + external.title, + description: external.url, + attachedSpace: pullRequest.space, + user: todoUser, + workslots: 0, + priority: ToDoPriority.High, + visibility: 'public', + rank: makeRank(undefined, latestTodo?.rank) + }, + undefined, + undefined, + account + ) + await this.client.createMixin( + todoId, + time.class.ToDo, + time.space.ToDos, + github.mixin.GithubTodo, + { + purpose: 'review' + }, + undefined, + account + ) + } + + private async requestFix ( + pullRequest: Pick< + GithubPullRequest, + '_id' | 'identifier' | 'reviewers' | 'title' | 'space' | 'state' | 'space' | '_class' + >, + external: PullRequestExternalData, + todoUser: Ref, + account: Ref + ): Promise { + const latestTodo = await this.client.findOne( + time.class.ToDo, + { + user: todoUser, + doneOn: null + }, + { + sort: { rank: SortingOrder.Ascending } + } + ) + + const todoId = await this.client.addCollection( + time.class.ProjectToDo, + time.space.ToDos, + pullRequest._id, + pullRequest._class, + 'todos', + { + attachedSpace: pullRequest.space, + title: 'Resolve ' + pullRequest.title, + description: external.url, + user: todoUser, + workslots: 0, + priority: ToDoPriority.High, + visibility: 'public', + rank: makeRank(undefined, latestTodo?.rank) + }, + undefined, + undefined, + account + ) + await this.client.createMixin( + todoId, + time.class.ToDo, + time.space.ToDos, + github.mixin.GithubTodo, + { + purpose: 'fix' + }, + undefined, + account + ) + } + + private async markDoneOrDeleteTodo (td: WithLookup): Promise { + // Let's mark as done in any case + await this.client.update(td, { + doneOn: Date.now() + }) + } + + async fillBackChanges (update: DocumentUpdate, existing: GithubIssue, external: any): Promise { + const statuses = await this.provider.getStatuses(existing.kind) + const status = (existing as unknown as GithubPullRequest).status + const pullRequestExternal = external as PullRequestExternalData + + // We need to update status in case category are different + const stInstance = + statuses.find((it) => it._id === status) ?? + ((await this.client.findOne(core.class.Status, { _id: status })) as Status) + + let gs: IssueStatus | undefined + if (pullRequestExternal.merged) { + gs = await guessStatus({ state: 'MERGED' }, statuses) + } + + // If PR is merged or closed, we need to update platform issue status + if (gs !== undefined && stInstance.category !== gs.category) { + update.status = gs._id + } + } + + async sync ( + existing: Doc | undefined, + info: DocSyncInfo, + parent: DocSyncInfo | undefined, + derivedClient: TxOperations + ): Promise | undefined> { + const container = await this.provider.getContainer(info.space) + if ( + container?.container === undefined || + ((container.project.projectNodeId === undefined || + !container.container.projectStructure.has(container.project._id)) && + syncConfig.MainProject) + ) { + return { needSync: '' } + } + + const pullRequestExternal = info.external as unknown as PullRequestExternalData + + if (info.externalVersion !== githubExternalSyncVersion) { + return { needSync: '' } + } + + let target = await this.getMilestoneIssueTarget( + container.project, + container.container, + existing as Issue, + pullRequestExternal + ) + if (target === null) { + // We need to wait, no milestone data yet. + return { needSync: '' } + } + if (target === undefined) { + target = this.getProjectIssueTarget(container.project, pullRequestExternal) + } + + const syncResult = await this.syncToTarget(target, container, existing, pullRequestExternal, derivedClient, info) + + return { + ...syncResult, + targetNodeId: target.target.projectNodeId + } + } + + async performIssueFieldsUpdate ( + info: DocSyncInfo, + existing: Issue, + platformUpdate: DocumentUpdate, + issueData: Pick, + container: ContainerFocus, + issueExternal: IssueExternalData, + okit: Octokit, + account: Ref + ): Promise { + let { state, body, ...issueUpdate } = await this.collectIssueUpdate( + info, + existing, + platformUpdate, + issueData, + container, + issueExternal, + github.class.GithubPullRequest + ) + + if ((issueExternal as PullRequestExternalData).merged) { + // We could not change state for merged pull requests. + state = undefined + } + + const hasFieldsUpdate = Object.keys(issueUpdate).length > 0 || state !== undefined + const isLocked = + info.isDescriptionLocked === true && !(await this.provider.isPlatformUser(account as Ref)) + + if (hasFieldsUpdate || body !== undefined) { + if (body !== undefined && !isLocked) { + await this.ctx.withLog( + '==> updatePullRequest', + {}, + async () => { + this.ctx.info('update-pr-fields', { + url: issueExternal.url, + ...issueUpdate, + body, + workspace: this.provider.getWorkspaceId().name + }) + if (isGHWriteAllowed()) { + await okit?.graphql( + ` + mutation updatePullRequest($issue: ID!, $body: String!) { + updatePullRequest(input: { + pullRequestId: $issue, + ${state !== undefined ? `state: ${state as string}` : ''} + ${gqlp(issueUpdate)}, + body: $body + }) { + pullRequest { + id + updatedAt + } + } + }`, + { issue: issueExternal.id, body } + ) + } + }, + { url: issueExternal.url } + ) + issueData.description = await this.provider.getMarkup(container.container, body, this.stripGuestLink) + } else if (hasFieldsUpdate) { + await this.ctx.withLog('==> updatePullRequest:', {}, async () => { + this.ctx.info('update-fields', { + url: issueExternal.url, + ...issueUpdate, + workspace: this.provider.getWorkspaceId().name + }) + if (isGHWriteAllowed()) { + await okit?.graphql( + ` + mutation updatePullRequest($issue: ID!) { + updatePullRequest(input: { + pullRequestId: $issue, + ${state !== undefined ? `state: ${state as string}` : ''} + ${gqlp(issueUpdate)} + }) { + pullRequest { + id + updatedAt + } + } + }`, + { issue: issueExternal.id } + ) + } + }) + } + return true + } + return false + } + + private async handlePatch ( + info: DocSyncInfo, + container: ContainerFocus, + pullRequestExternal: PullRequestExternalData, + existingPR: Pick, + lastModified: number, + account: Ref + ): Promise { + let patch: string | null = null + const repo = container.repository.find((it) => it._id === info.repository) + if (repo?.nodeId === undefined) { + return null + } + if (info.external?.patch !== true) { + patch = await this.fetchPatch(pullRequestExternal, container.container.octokit, repo) + + // Update attached patch data. + const patchAttachment = await this.client.findOne(github.class.GithubPatch, { attachedTo: existingPR._id }) + const blob = await this.provider.uploadFile(patch, patchAttachment?.file) + if (blob !== undefined) { + if (patchAttachment === undefined) { + await this.client.addCollection( + github.class.GithubPatch, + existingPR.space, + existingPR._id, + existingPR._class, + 'attachments', + { + name: 'Patch.diff', + file: blob._id, + type: blob.contentType, + size: blob.size, + lastModified, + readonly: true + }, + generateId(), + lastModified, + account + ) + } else { + await this.client.diffUpdate( + patchAttachment, + { + size: blob.size, + type: blob.contentType, + lastModified + }, + new Date().getTime(), + account + ) + } + } + } + return patch + } + + private async createPullRequest ( + info: DocSyncInfo, + account: Ref, + pullRequestData: GithubPullRequestData & { status: Issue['status'] }, + pullRequestExternal: PullRequestExternalData, + repo: Ref, + prj: GithubProject, + taskType: Ref, + repository: GithubIntegrationRepository, + isDescriptionLocked: boolean + ): Promise { + const lastOne = await this.client.findOne( + tracker.class.Issue, + { space: prj._id }, + { sort: { rank: SortingOrder.Descending }, limit: 1 } + ) + const incResult = await this.client.updateDoc( + tracker.class.Project, + core.space.Space, + info.space as Ref, + { $inc: { sequence: 1 } }, + true, + new Date().getTime(), + account + ) + + const project = (incResult as any).object as Project + const number = project.sequence + const value: AttachedData = { + ...pullRequestData, + kind: taskType, + component: null, + milestone: null, + number, + identifier: `${project.identifier}-${number}`, + priority: IssuePriority.Medium, + rank: calcRank(lastOne, undefined), + comments: 0, + subIssues: 0, + dueDate: null, + parents: [], + reportedTime: 0, + estimation: 0, + remainingTime: 0, + reports: 0, + relations: [], + childInfo: [], + commits: 0, + reviewComments: 0, + reviews: 0 + } + + const prId = info._id as unknown as Ref + await this.client.addCollection( + github.class.GithubPullRequest, + info.space, + tracker.ids.NoParent, + tracker.class.Issue, + 'subIssues', + value, + prId, + new Date(pullRequestExternal.createdAt).getTime(), + account + ) + await this.client.createMixin( + prId, + github.class.GithubPullRequest, + info.space, + github.mixin.GithubIssue, + { + githubNumber: pullRequestExternal.number, + url: pullRequestExternal.url, + repository: repo, + descriptionLocked: isDescriptionLocked + } + ) + await this.client.createMixin(prId, github.mixin.GithubIssue, prj._id, prj.mixinClass, {}) + + await this.addConnectToMessage( + github.string.PullRequestConnectedActivityInfo, + prj._id, + prId, + tracker.class.Issue, + pullRequestExternal, + repository + ) + } + + async externalSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + kind: ExternalSyncField, + syncDocs: DocSyncInfo[], + repo: GithubIntegrationRepository, + prj: GithubProject + ): Promise { + if (kind === 'externalVersion') { + // Bulk update of selected PR's + // Wait global project sync + await this.performExternalSync(integration, prj, syncDocs, repo, derivedClient) + } + + if (kind === 'derivedVersion') { + // Perform external synchronization's + // TODO: Add re-request for missing reviews/review threads. + await this.performDerivedSync(syncDocs, derivedClient, prj, repo) + } + } + + private async performDerivedSync ( + syncDocs: DocSyncInfo[], + derivedClient: TxOperations, + prj: GithubProject, + repo: GithubIntegrationRepository + ): Promise { + for (const d of syncDocs) { + const ext = d.external as PullRequestExternalData + if (ext == null) { + continue + } + if (ext.reviews.nodes.length < ext.reviews.totalCount) { + // TODO: We need to fetch missing items. + } + + if (ext.reviewThreads.nodes.length < ext.reviewThreads.totalCount) { + // TODO: We need to fetch missing items. + } + + await syncDerivedDocuments( + derivedClient, + d, + ext, + prj, + repo, + github.class.GithubReview, + {}, + (ext) => ext.reviews.nodes + ) + await syncDerivedDocuments(derivedClient, d, ext, prj, repo, github.class.GithubReviewThread, {}, (ext) => + ext.reviewThreads.nodes.map((it) => ({ + ...it, + url: it.id, + createdAt: new Date(it.comments.nodes[0].createdAt ?? Date.now()).toISOString(), + updatedAt: new Date(getUpdatedAtReviewThread(it)).toISOString() + })) + ) + } + + const tx = derivedClient.apply('pullrequests_github') + for (const d of syncDocs) { + await tx.update(d, { derivedVersion: githubDerivedSyncVersion }) + } + await tx.commit() + this.provider.sync() + } + + private async performExternalSync ( + integration: IntegrationContainer, + prj: GithubProject, + syncDocs: DocSyncInfo[], + repo: GithubIntegrationRepository, + derivedClient: TxOperations + ): Promise { + await integration.syncLock.get(prj._id) + + const ids = syncDocs.map((it) => (it.external as IssueExternalData).id).filter((it) => it !== undefined) + + let partsize = 50 + try { + while (true) { + const idsPart = ids.splice(0, partsize) + if (idsPart.length === 0) { + break + } + const idsp = idsPart.map((it) => `"${it}"`).join(', ') + try { + const response: any = await this.ctx.withLog( + 'fetch pull request updates', + {}, + async () => + await integration.octokit.graphql( + `query listIssues { + nodes(ids: [${idsp}] ) { + ... on PullRequest { + ${pullRequestDetails} + } + } + }` + ), + { + prj: prj.name, + repo: repo.name, + ids: idsp + } + ) + const issues: PullRequestExternalData[] = response.nodes + + if (issues.some((issue) => issue.url === undefined && Object.keys(issue).length === 0)) { + this.ctx.error('empty document content updates', { + repo: repo.name, + workspace: this.provider.getWorkspaceId().name, + data: cutObjectArray(response) + }) + } + await this.syncIssues(github.class.GithubPullRequest, repo, issues, derivedClient) + } catch (err: any) { + if (partsize > 1) { + partsize = 1 + ids.push(...idsPart) + this.ctx.warn('pull request external retrieval switch to one by one mode', { + errors: err.errors, + msg: err.message, + workspace: this.provider.getWorkspaceId().name + }) + } else if (partsize === 1) { + // We need to update issue, since it is missing on external side. + const syncDoc = syncDocs.find((it) => it.external.id === idsPart[0]) + if (syncDoc !== undefined) { + this.ctx.warn('mark missing external PR', { + errors: err.errors, + msg: err.message, + url: syncDoc.url, + workspace: this.provider.getWorkspaceId().name + }) + await derivedClient.diffUpdate( + syncDoc, + { + needSync: githubSyncVersion, + externalVersion: githubExternalSyncVersion, + derivedVersion: githubDerivedSyncVersion + }, + Date.now() + ) + } + } + } + } + for (const d of syncDocs) { + if ((d.external as IssueExternalData).id == null) { + this.ctx.error('failed to do external sync for', { objectClass: d.objectClass, _id: d._id }) + // no external data for doc + await derivedClient.update(d, { + externalVersion: githubExternalSyncVersion + }) + } + } + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + } + this.provider.sync() + } + + repositoryDisabled (integration: IntegrationContainer, repo: GithubIntegrationRepository): void { + integration.synchronized.delete(`${repo._id}:pullRequests`) + } + + async externalFullSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + projects: GithubProject[], + repositories: GithubIntegrationRepository[] + ): Promise { + for (const repo of repositories) { + const prj = projects.find((it) => repo.githubProject === it._id) + if (prj === undefined) { + continue + } + // Wait global project sync + await integration.syncLock.get(prj._id) + + const syncKey = `${repo._id}:pullRequests` + if ( + repo.githubProject === undefined || + !repo.enabled || + integration.synchronized.has(syncKey) || + integration.octokit === undefined + ) { + if (!repo.enabled) { + integration.synchronized.delete(syncKey) + } + continue + } + const since = await getSinceRaw(this.client, github.class.GithubPullRequest, repo) + + // We need always sync open PRs, since review changes are not included into PR updated state. + this.ctx.info('sync external pull requests', { + repo: repo.name, + since, + workspace: this.provider.getWorkspaceId().name, + state: 'OPEN' + }) + await this.performPRSync(integration, repo, 'OPEN', undefined, derivedClient, prj) + + this.ctx.info('sync external pull requests', { + repo: repo.name, + since, + workspace: this.provider.getWorkspaceId().name, + state: 'CLOSED, MERGED' + }) + await this.performPRSync(integration, repo, 'CLOSED, MERGED', since, derivedClient, prj) + + this.ctx.info('sync external pull requests - done', { + repo: repo.name, + since, + workspace: this.provider.getWorkspaceId().name + }) + + this.provider.sync() + integration.synchronized.add(syncKey) + } + } + + private async performPRSync ( + integration: IntegrationContainer, + repo: GithubIntegrationRepository, + states: string, + since: number | undefined, + derivedClient: TxOperations, + prj: GithubProject + ): Promise { + try { + const pullRequestIterator = integration.octokit.graphql.paginate.iterator( + `query listPullRequests($name: String!, $owner: String!, $cursor: String) { + repository(name: $name, owner: $owner) { + pullRequests( + first: 25, + orderBy: {field: UPDATED_AT, direction: DESC}, + states: [${states}], + after: $cursor) { + nodes { + ${pullRequestDetails} + } + pageInfo { + startCursor + hasNextPage + endCursor + } + totalCount + } + } + } + `, + { + name: repo.name, + owner: repo.owner?.login ?? '' + } + ) + for await (const data of pullRequestIterator) { + const issues: PullRequestExternalData[] = data.repository.pullRequests.nodes + this.ctx.info('retrieve pull requests for', { + repo: repo.name, + since, + len: issues.length, + workspace: this.provider.getWorkspaceId().name + }) + + if (since !== undefined) { + // Check if since > all updated data, then break and store since. + const hasUpdated = issues.some((it) => new Date(it.updatedAt).getTime() > since) + if (!hasUpdated) { + // We updated all since documents already + break + } + } + + let emptyIndex = -1 + emptyIndex = issues.findIndex((issue) => issue.url === undefined && Object.keys(issue).length === 0) + if (emptyIndex !== -1) { + this.ctx.error('empty document content', { + repo: repo.name, + workspace: this.provider.getWorkspaceId().name, + data: cutObjectArray(data), + emptyIndex, + el: JSON.stringify(issues[emptyIndex]) + }) + } + + await this.syncIssues(github.class.GithubPullRequest, repo, issues, derivedClient) + } + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + } + } + + async fetchPatch ( + pullRequest: PullRequestExternalData, + octokit: Octokit, + repository: GithubIntegrationRepository + ): Promise { + let patch = '' + try { + const patchContent = await octokit.rest.pulls.get({ + owner: repository.owner?.login as string, + repo: repository.name, + pull_number: pullRequest.number, + headers: { + Accept: 'application/vnd.github.VERSION.diff', + 'X-GitHub-Api-Version': '2022-11-28' + } + }) + patch = ((patchContent as unknown as OctokitResponse).data ?? '').slice(0, 2 * 1024 * 1024) + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + } + return patch + } + + async deleteGithubDocument (container: ContainerFocus, account: Ref, id: string): Promise { + // No delete is allowed for pull requests + } +} diff --git a/services/github/pod-github/src/sync/repository.ts b/services/github/pod-github/src/sync/repository.ts new file mode 100644 index 0000000000..ce5bb45a20 --- /dev/null +++ b/services/github/pod-github/src/sync/repository.ts @@ -0,0 +1,327 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// + +import core, { Doc, DocData, DocumentUpdate, MeasureContext, TxOperations, generateId } from '@hcengineering/core' +import { Endpoints } from '@octokit/types' +import { Repository, RepositoryEvent } from '@octokit/webhooks-types' +import github, { DocSyncInfo, GithubIntegrationRepository, GithubProject } from '@hcengineering/github' +import { App } from 'octokit' +import { DocSyncManager, ExternalSyncField, IntegrationContainer, IntegrationManager } from '../types' +import { collectUpdate } from './utils' + +const syncReposKey = 'repo_sync' + +export class RepositorySyncMapper implements DocSyncManager { + constructor ( + private readonly ctx: MeasureContext, + private readonly client: TxOperations, + private readonly app: App + ) {} + + externalDerivedSync = false + + provider!: IntegrationManager + + // Initialize the mapper. + async init (provider: IntegrationManager): Promise { + this.provider = provider + } + + // Perform synchronization of document with external source. + async sync (existing: Doc | undefined, info: DocSyncInfo): Promise | undefined> { + return {} + } + + async reloadRepositories (integration: IntegrationContainer): Promise { + integration.synchronized.delete(syncReposKey) + } + + async handleEvent(integration: IntegrationContainer, derivedClient: TxOperations, evt: T): Promise { + const event = evt as RepositoryEvent + + const account = (await this.provider.getAccountU(event.sender))?._id ?? core.account.System + switch (event.action) { + case 'created': { + await this.client.addCollection( + github.class.GithubIntegrationRepository, + integration.integration.space, + integration.integration._id, + integration.integration._class, + 'repositories', + { + ...this.getRData(event.repository), + name: event.repository.name, + repositoryId: event.repository.id, + enabled: true + }, + generateId(), + Date.now(), + account + ) + this.ctx.info('Creating repository info document...', { + url: event.repository.url, + workspace: this.provider.getWorkspaceId().name + }) + break + } + case 'renamed': { + const githubRepo = await this.client.findOne(github.class.GithubIntegrationRepository, { + repositoryId: event.repository.id + }) + if (githubRepo !== undefined) { + await this.client.update( + githubRepo, + { + name: event.repository.name + }, + false, + Date.now(), + account + ) + githubRepo.name = event.repository.name + const allProjects = await this.client.findAll(github.mixin.GithubProject, { repositories: githubRepo?._id }) + for (const prj of allProjects) { + // We need to force sync + await this.handleRepoRename(integration, prj, githubRepo) + } + } + + break + } + case 'deleted': + case 'transferred': { + // TODO: Add remove of component + const githubRepo = await this.client.findOne(github.class.GithubIntegrationRepository, { + integration: integration.integration._id, + name: event.repository.name + }) + if (githubRepo !== undefined) { + await this.client.update( + githubRepo, + { + enabled: true, + deleted: true + }, + false, + Date.now(), + account + ) + } + } + } + } + + async handleDelete ( + existing: Doc | undefined, + info: DocSyncInfo, + derivedClient: TxOperations, + deleteExisting: boolean + ): Promise { + return false + } + + getRData ( + repository: Repository | Endpoints['GET /installation/repositories']['response']['data']['repositories'][0] + ): Omit, 'name' | 'repositoryId' | 'deleted' | 'githubProjects' | 'enabled'> { + return { + nodeId: repository.node_id, + url: repository.url, + htmlURL: repository.html_url, + owner: { + id: repository.owner.node_id, + login: repository.owner.login, + avatarUrl: repository.owner.avatar_url, + email: repository.owner.email ?? undefined, + name: repository.owner.name ?? undefined + }, + description: repository.description ?? undefined, + fork: repository.fork, + forks: repository.forks, + private: repository.private, + stargazers: repository.stargazers_count, + + hasIssues: repository.has_issues, + hasProjects: repository.has_projects, + hasDownloads: repository.has_downloads, + hasPages: repository.has_pages, + hasWiki: repository.has_wiki, + hasDiscussions: repository.has_discussions ?? false, + + openIssues: repository.open_issues, + watchers: repository.watchers_count, + archived: repository.archived, + size: repository.size, + language: repository.language ?? undefined, + resourcePath: repository.full_name, + + visibility: repository.visibility, + updatedAt: new Date(repository.updated_at ?? repository.created_at ?? Date.now()).getTime() + } + } + + async externalSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + kind: ExternalSyncField, + syncDocs: DocSyncInfo[], + repo: GithubIntegrationRepository, + prj: GithubProject + ): Promise {} + + repositoryDisabled (integration: IntegrationContainer, repo: GithubIntegrationRepository): void {} + + async externalFullSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + projects: GithubProject[], + repositories: GithubIntegrationRepository[] + ): Promise { + const inst = integration.octokit + if (inst === undefined || integration.octokit === undefined) { + this.ctx.info('no installation found', { workspace: this.provider.getWorkspaceId().name }) + return + } + + if (integration.synchronized.has(syncReposKey)) { + return + } + this.ctx.info('Checking github installation repositories...', { + installationId: integration.installationId, + workspace: this.provider.getWorkspaceId().name + }) + const iterable = this.app.eachRepository.iterator({ installationId: integration.installationId }) + + // Need to find all repositories, not only active, so passed repositories are not work. + const allRepositories = ( + await this.provider.liveQuery.queryFind(github.class.GithubIntegrationRepository, {}) + ).filter((it) => it.attachedTo === integration.integration._id) + + let allRepos: GithubIntegrationRepository[] = [...allRepositories] + + for await (const { repository } of iterable) { + const integrationRepo: GithubIntegrationRepository | undefined = allRepos.find( + (it) => it.repositoryId === repository.id + ) + + const rdata = this.getRData(repository) + if (integrationRepo === undefined) { + // No integration repository found, we need to push one. + await this.client.addCollection( + github.class.GithubIntegrationRepository, + integration.integration.space, + integration.integration._id, + integration.integration._class, + 'repositories', + { + ...rdata, + name: repository.name, + repositoryId: repository.id, + enabled: true, + deleted: false + }, + undefined, // id + Date.now(), + integration.integration.createdBy + ) + this.ctx.info('Creating repository info document...', { + url: repository.url, + workspace: this.provider.getWorkspaceId().name + }) + } else { + allRepos = allRepos.filter((it) => it._id !== integrationRepo._id) + const diff = collectUpdate( + integrationRepo, + { + name: repository.name, + ...rdata + }, + ['name', ...Object.keys(rdata)] + ) + if (Object.keys(diff).length > 0) { + this.ctx.info('processing repository diff update...', { + repository: repository.name, + ...diff, + workspace: this.provider.getWorkspaceId().name + }) + await this.client.diffUpdate( + integrationRepo, + { + name: repository.name, + ...rdata + }, + new Date().getTime(), + integration.integration.createdBy + ) + } + } + } + + // Ok we have repos removed from integration, we need to delete them. + for (const repo of allRepos) { + await this.client.remove(repo) + const prj = projects.find((it) => it._id === repo.githubProject) + if (prj !== undefined) { + await this.client.update(prj, { + $pull: { repositories: repo._id } + }) + } + } + + // We need to delete and disconnect missing repositories. + + integration.synchronized.add(syncReposKey) + } + + // Perform a synchronization of a single repository. + async handleRepoRename ( + integration: IntegrationContainer, + prj: GithubProject, + repo: GithubIntegrationRepository + ): Promise { + // We need to update urls for all sync documents belong to this repository. + + const derivedClient = new TxOperations(this.client, core.account.System, true) + const processingId = generateId() + + // Wait previous sync to finish + await integration.syncLock.get(prj._id) + + /** + Variants: + "https://api.github.com/repos/hcengineering/anticrm/issues/comments/1679316918" + "https://github.com/hcengineering/uberflow/pull/195" + * */ + this.ctx.info('handle repository rename', { repo, workspace: this.provider.getWorkspaceId().name }) + const update = async (): Promise => { + while (true) { + const docs = await this.client.findAll( + github.class.DocSyncInfo, + { _class: github.class.DocSyncInfo, repository: repo._id, processingId: { $ne: processingId } }, + { limit: 1000 } + ) + const ops = derivedClient.apply(repo._id) + if (docs.length === 0) { + break + } + for (const d of docs) { + const ul = d.url.split('/') + if (ul[2] === 'api.github.com') { + ul[5] = repo.name + } else { + ul[4] = repo.name + } + // We need to mark sync is required, to perform github + await ops.diffUpdate(d, { url: ul.join('/'), processingId, needSync: '', externalVersion: '' }) + } + await ops.commit() + this.provider.sync() + } + } + const p = update() + integration.syncLock.set(prj._id, p) + await p + integration.syncLock.delete(prj._id) + } +} diff --git a/services/github/pod-github/src/sync/reviewComments.ts b/services/github/pod-github/src/sync/reviewComments.ts new file mode 100644 index 0000000000..27b9415ec7 --- /dev/null +++ b/services/github/pod-github/src/sync/reviewComments.ts @@ -0,0 +1,581 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +import { PersonAccount } from '@hcengineering/contact' +import core, { + Account, + AttachedData, + Doc, + DocData, + DocumentUpdate, + MeasureContext, + Ref, + TxOperations +} from '@hcengineering/core' +import { LiveQuery } from '@hcengineering/query' +import github, { + DocSyncInfo, + GithubIntegrationRepository, + GithubProject, + GithubReviewComment +} from '@hcengineering/github' +import { + ContainerFocus, + DocSyncManager, + ExternalSyncField, + IntegrationContainer, + IntegrationManager, + githubExternalSyncVersion, + githubSyncVersion +} from '../types' +import { ReviewComment as ReviewCommentExternalData, reviewCommentDetails } from './githubTypes' +import { collectUpdate, deleteObjects, errorToObj, isGHWriteAllowed } from './utils' + +import { Analytics } from '@hcengineering/analytics' +import { PullRequestReviewCommentCreatedEvent, PullRequestReviewCommentEvent } from '@octokit/webhooks-types' +import config from '../config' +import { syncConfig } from './syncConfig' + +export type ReviewCommentData = DocData + +export class ReviewCommentSyncManager implements DocSyncManager { + provider!: IntegrationManager + + createCommentPromise: Promise> | undefined + + externalDerivedSync = false + + constructor ( + readonly ctx: MeasureContext, + readonly client: TxOperations, + readonly lq: LiveQuery + ) {} + + async init (provider: IntegrationManager): Promise { + this.provider = provider + } + + eventSync = new Map>() + async handleEvent(integration: IntegrationContainer, derivedClient: TxOperations, evt: T): Promise { + await this.createCommentPromise + const event = evt as PullRequestReviewCommentEvent + + if (event.sender.type === 'Bot') { + // Ignore events from Bot if it is our bot + // No need to handle event from ourself + if (event.sender.login.includes(config.BotName)) { + return + } + } + this.ctx.info('reviewComments:handleEvent', { + action: event.action, + login: event.sender.login, + workspace: this.provider.getWorkspaceId().name + }) + const { project, repository } = await this.provider.getProjectAndRepository(event.repository.node_id) + if (project === undefined || repository === undefined) { + this.ctx.info('No project for repository', { + name: event.repository.name, + workspace: this.provider.getWorkspaceId().name + }) + return + } + await this.eventSync.get(event.comment.html_url) + const promise = this.processEvent(event, derivedClient, repository, integration) + this.eventSync.set(event.comment.html_url, promise) + await promise + this.eventSync.delete(event.comment.html_url) + } + + async handleDelete ( + existing: Doc | undefined, + info: DocSyncInfo, + derivedClient: TxOperations, + deleteExisting: boolean, + parent?: DocSyncInfo + ): Promise { + const container = await this.provider.getContainer(info.space) + if (container === undefined) { + return false + } + if ( + container?.container === undefined || + ((container.project.projectNodeId === undefined || + !container.container.projectStructure.has(container.project._id)) && + syncConfig.MainProject) + ) { + return false + } + + const commentExternal = info.external + + if (commentExternal === undefined) { + // No external issue yet, safe delete, since platform document will be deleted a well. + return true + } + const account = + existing?.createdBy ?? (await this.provider.getAccountU(commentExternal.user))?._id ?? core.account.System + + if (commentExternal !== undefined) { + try { + await this.deleteGithubDocument(container, account, commentExternal.node_id, derivedClient, parent) + } catch (err: any) { + let cnt = false + if (Array.isArray(err.errors)) { + for (const e of err.errors) { + if (e.type === 'NOT_FOUND') { + // Ok issue is already deleted + cnt = true + break + } + } + } + if (!cnt) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + await derivedClient.update(info, { error: errorToObj(err) }) + } + } + } + + if (existing !== undefined && deleteExisting) { + await deleteObjects(this.ctx, this.client, [existing], account) + } + return true + } + + async deleteGithubDocument ( + container: ContainerFocus, + account: Ref, + id: string, + derivedClient: TxOperations, + parent?: DocSyncInfo + ): Promise { + const okit = (await this.provider.getOctokit(account as Ref)) ?? container.container.octokit + const q = `mutation deleteReviewComment($reviewID: ID!) { + deletePullRequestReviewComment(input: { + id: $reviewID + }) { + pullRequestReview { + url + } + } + }` + if (isGHWriteAllowed()) { + await okit?.graphql(q, { + reviewID: id + }) + } + if (parent !== undefined) { + // We need to force pull request update to sync review content properly. + await derivedClient.update(parent, { externalVersion: '', derivedVersion: '' }) + } + } + + private async processEvent ( + event: PullRequestReviewCommentEvent, + derivedClient: TxOperations, + repo: GithubIntegrationRepository, + integration: IntegrationContainer + ): Promise { + const account = (await this.provider.getAccountU(event.sender))?._id ?? core.account.System + + let externalData: ReviewCommentExternalData + try { + const response: any = await integration.octokit?.graphql( + ` + query listReview($reviewID: ID!) { + node(id: $reviewID) { + ... on PullRequestReviewComment { + ${reviewCommentDetails} + } + } + } + `, + { + reviewID: event.comment.node_id + } + ) + externalData = response.node + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + return + } + if (externalData === undefined) { + return + } + + switch (event.action) { + case 'created': { + await this.createSyncData(event, derivedClient, repo, externalData) + break + } + case 'deleted': { + const reviewData = await this.client.findOne(github.class.DocSyncInfo, { + url: (event.comment.html_url ?? '').toLowerCase() + }) + if (reviewData !== undefined) { + await derivedClient.update( + reviewData, + { + deleted: true, + needSync: '' + }, + false, + Date.now(), + account + ) + this.provider.sync() + } + break + } + case 'edited': { + const reviewData = await this.client.findOne(github.class.DocSyncInfo, { + url: (event.comment.html_url ?? '').toLowerCase() + }) + + if (reviewData !== undefined) { + const reviewObj: GithubReviewComment | undefined = await this.client.findOne( + reviewData.objectClass, + { + _id: reviewData._id as unknown as Ref + } + ) + if (reviewObj !== undefined) { + const lastModified = Date.now() + const body = await this.provider.getMarkup(integration, event.comment.body) + await derivedClient.diffUpdate( + reviewData, + { + external: externalData, + externalVersion: githubExternalSyncVersion, + current: { ...reviewData.current, body }, + needSync: githubSyncVersion, + lastModified + }, + lastModified + ) + await this.client.update( + reviewObj, + { + body + }, + false, + lastModified, + account + ) + this.provider.sync() + } + } + break + } + } + } + + private async createSyncData ( + createdEvent: PullRequestReviewCommentCreatedEvent, + derivedClient: TxOperations, + repo: GithubIntegrationRepository, + externalData: ReviewCommentExternalData + ): Promise { + const reviewData = await this.client.findOne(github.class.DocSyncInfo, { + url: (createdEvent.comment.html_url ?? '').toLowerCase() + }) + + if (reviewData === undefined) { + await derivedClient.createDoc(github.class.DocSyncInfo, repo.githubProject as Ref, { + url: (createdEvent.comment.html_url ?? '').toLowerCase(), + needSync: '', + githubNumber: 0, + repository: repo._id, + objectClass: github.class.GithubReviewComment, + external: externalData, + externalVersion: githubExternalSyncVersion, + parent: createdEvent.pull_request.html_url, + lastModified: new Date(createdEvent.comment.updated_at ?? Date.now()).getTime() + }) + this.provider.sync() + } + } + + async sync ( + existing: Doc | undefined, + info: DocSyncInfo, + parent: DocSyncInfo | undefined, + derivedClient: TxOperations + ): Promise | undefined> { + const container = await this.provider.getContainer(info.space) + if (container?.container === undefined) { + return {} + } + if (parent === undefined) { + return { needSync: '' } + } + if (info.external === undefined) { + // TODO: Use selected repository + const repo = container.repository.find((it) => it._id === parent?.repository) + if (repo?.nodeId === undefined) { + // No need to sync if parent repository is not defined. + return { needSync: githubSyncVersion } + } + + // If no external document, we need to create it. + this.createCommentPromise = this.createGithubReviewComment(container, existing, info, parent, derivedClient) + return await this.createCommentPromise + } + const reviewComment = info.external as ReviewCommentExternalData + + const account = + existing?.modifiedBy ?? (await this.provider.getAccount(reviewComment.author))?._id ?? core.account.System + + if (info.reviewThreadId === undefined && reviewComment.replyTo?.url !== undefined) { + const rthread = await derivedClient.findOne(github.class.GithubReviewComment, { + url: reviewComment.replyTo?.url?.toLowerCase() + }) + if (rthread !== undefined) { + info.reviewThreadId = rthread.reviewThreadId + await derivedClient.update(info, { reviewThreadId: info.reviewThreadId }) + } + } + + const messageData: ReviewCommentData = { + body: await this.provider.getMarkup(container.container, reviewComment.body), + diffHunk: reviewComment.diffHunk, + isMinimized: reviewComment.isMinimized, + reviewUrl: reviewComment.pullRequestReview.url, + line: reviewComment.line, + startLine: reviewComment.startLine, + originalLine: reviewComment.originalLine, + outdated: reviewComment.outdated, + path: reviewComment.path, + url: reviewComment.url.toLowerCase(), + minimizedReason: reviewComment.minimizedReason, + includesCreatedEdit: reviewComment.includesCreatedEdit, + originalStartLine: reviewComment.originalLine, + replyToUrl: reviewComment.replyTo?.url, + reviewThreadId: info.reviewThreadId + } + if (existing === undefined) { + try { + await this.createReviewComment(info, messageData, parent, reviewComment, account) + return { needSync: githubSyncVersion, current: messageData } + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + return { needSync: githubSyncVersion, error: errorToObj(err) } + } + } else { + await this.handleDiffUpdate(existing, info, messageData, container, parent, reviewComment, account, derivedClient) + } + return { current: messageData, needSync: githubSyncVersion } + } + + private async handleDiffUpdate ( + existing: Doc, + info: DocSyncInfo, + reviewCommentData: ReviewCommentData, + container: ContainerFocus, + parent: DocSyncInfo, + reviewComment: ReviewCommentExternalData, + account: Ref, + derivedClient: TxOperations + ): Promise { + const repository = container.repository.find((it) => it._id === info.repository) + if (repository === undefined) { + return + } + + const existingReview = existing as GithubReviewComment + + const previousData: ReviewCommentData = info.current ?? ({} as unknown as ReviewCommentData) + + const update = collectUpdate(previousData, reviewCommentData, Object.keys(reviewCommentData)) + + const platformUpdate = collectUpdate(previousData, existing, Object.keys(reviewCommentData)) + + // We should remove changes we already have from github changed. + for (const [k, v] of Object.entries(update)) { + if ((platformUpdate as any)[k] !== v) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (platformUpdate as any)[k] + } + } + // Remove current same values from update + for (const [k, v] of Object.entries(existingReview)) { + if ((update as any)[k] === v) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (update as any)[k] + } + } + + if (Object.keys(platformUpdate).length > 0) { + if (platformUpdate.body !== undefined) { + const body = await this.provider.getMarkup(container.container, platformUpdate.body) + const okit = (await this.provider.getOctokit(account as Ref)) ?? container.container.octokit + const q = `mutation updateReviewComment($commentID: ID!, $body: String!) { + updatePullRequestReviewComment(input: { + threadId: $threadID + }) { + pullRequestReviewComment { + id + } + }` + if (isGHWriteAllowed()) { + await okit?.graphql(q, { + threadID: reviewComment.id, + body + }) + } + await derivedClient.update(info, { external: { ...info.external, body } }) + } + } + if (Object.keys(update).length > 0) { + await this.client.update( + existing, + update, + false, + new Date(reviewComment.updatedAt ?? Date.now()).getTime(), + account + ) + } + } + + private async createReviewComment ( + info: DocSyncInfo, + messageData: ReviewCommentData, + parent: DocSyncInfo, + review: ReviewCommentExternalData, + account: Ref + ): Promise { + const _id: Ref = info._id as unknown as Ref + const value: AttachedData = { + ...messageData + } + await this.client.addCollection( + github.class.GithubReviewComment, + info.space, + parent._id, + parent.objectClass, + 'reviewComments', + value, + _id, + new Date(review.createdAt ?? Date.now()).getTime(), + account + ) + } + + async createGithubReviewComment ( + container: ContainerFocus, + existing: Doc | undefined, + info: DocSyncInfo, + parent: DocSyncInfo, + derivedClient: TxOperations + ): Promise> { + // TODO: Use selected repository + const repo = container.repository.find((it) => it._id === parent?.repository) + if (repo?.nodeId === undefined) { + // No need to sync if parent repository is not defined. + return { needSync: githubSyncVersion } + } + + if (parent === undefined) { + return {} + } + const existingReview = existing as GithubReviewComment + const okit = + (await this.provider.getOctokit(existingReview.modifiedBy as Ref)) ?? container.container.octokit + + // No external version yet, create it. + try { + const q = `mutation createComment($prID: ID!, $body: String!) { + addPullRequestReviewThreadReply(input:{ + pullRequestReviewThreadId: $prID, + body: $body + }) { + comment { + ${reviewCommentDetails} + } + } + }` + + if (isGHWriteAllowed()) { + const response: + | { + addPullRequestReviewThreadReply: { + comment: ReviewCommentExternalData + } + } + | undefined = await okit?.graphql(q, { + prID: existingReview.reviewThreadId, + body: (await this.provider.getMarkdown(existingReview.body)) ?? '' + }) + + const reviewExternal = response?.addPullRequestReviewThreadReply?.comment + + if (reviewExternal !== undefined) { + const upd: DocumentUpdate = { + url: reviewExternal.url.toLowerCase(), + external: reviewExternal, + current: existing, + repository: repo._id, + needSync: githubSyncVersion, + externalVersion: githubExternalSyncVersion + } + // We need to update in current promise, to prevent event changes. + await derivedClient.update(info, upd) + + await this.client.update(existingReview, { + diffHunk: reviewExternal.diffHunk, + isMinimized: reviewExternal.isMinimized, + reviewUrl: reviewExternal.pullRequestReview.url, + line: reviewExternal.line, + startLine: reviewExternal.startLine, + originalLine: reviewExternal.originalLine, + outdated: reviewExternal.outdated, + path: reviewExternal.path, + url: reviewExternal.url.toLowerCase(), + minimizedReason: reviewExternal.minimizedReason, + includesCreatedEdit: reviewExternal.includesCreatedEdit, + originalStartLine: reviewExternal.originalLine, + replyToUrl: reviewExternal.replyTo?.url, + reviewThreadId: info.reviewThreadId ?? existingReview.reviewThreadId + }) + } + } + return {} + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + return { needSync: githubSyncVersion, error: errorToObj(err) } + } + } + + async externalSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + kind: ExternalSyncField, + syncDocs: DocSyncInfo[], + repository: GithubIntegrationRepository, + project: GithubProject + ): Promise { + // No need to perform external sync for reviews, so let's update marks + const tx = derivedClient.apply('reviews_github') + for (const d of syncDocs) { + await tx.update(d, { externalVersion: githubExternalSyncVersion }) + } + await tx.commit() + this.provider.sync() + } + + repositoryDisabled (integration: IntegrationContainer, repo: GithubIntegrationRepository): void {} + + async externalFullSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + projects: GithubProject[], + repositories: GithubIntegrationRepository[] + ): Promise { + // No external sync for reviews, they are done in pull requests. + } +} diff --git a/services/github/pod-github/src/sync/reviewThreads.ts b/services/github/pod-github/src/sync/reviewThreads.ts new file mode 100644 index 0000000000..bd6b733c29 --- /dev/null +++ b/services/github/pod-github/src/sync/reviewThreads.ts @@ -0,0 +1,565 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +import { PersonAccount } from '@hcengineering/contact' +import core, { + Account, + AttachedData, + Doc, + DocumentUpdate, + MeasureContext, + Ref, + TxOperations +} from '@hcengineering/core' +import { EmptyMarkup } from '@hcengineering/text' +import { LiveQuery } from '@hcengineering/query' +import github, { + DocSyncInfo, + GithubIntegrationRepository, + GithubProject, + GithubReviewThread +} from '@hcengineering/github' +import { + ContainerFocus, + DocSyncManager, + ExternalSyncField, + IntegrationContainer, + IntegrationManager, + githubDerivedSyncVersion, + githubExternalSyncVersion, + githubSyncVersion +} from '../types' +import { + PullRequestExternalData, + ReviewThread as ReviewThreadExternalData, + getUpdatedAtReviewThread, + reviewThreadDetails +} from './githubTypes' +import { collectUpdate, deleteObjects, errorToObj, isGHWriteAllowed, syncDerivedDocuments } from './utils' + +import { Analytics } from '@hcengineering/analytics' +import { PullRequestReviewThreadEvent } from '@octokit/webhooks-types' +import config from '../config' +import { syncConfig } from './syncConfig' + +export type ReviewThreadData = Pick< +GithubReviewThread, +| 'threadId' +| 'line' +| 'diffSide' +| 'startLine' +| 'isCollapsed' +| 'isPinned' +| 'isResolved' +| 'isOutdated' +| 'path' +| 'originalLine' +| 'originalStartLine' +| 'resolvedBy' +| 'startDiffSide' +> + +export class ReviewThreadSyncManager implements DocSyncManager { + provider!: IntegrationManager + + createCommentPromise: Promise> | undefined + + externalDerivedSync = true + + constructor ( + readonly ctx: MeasureContext, + readonly client: TxOperations, + readonly lq: LiveQuery + ) {} + + async init (provider: IntegrationManager): Promise { + this.provider = provider + } + + eventSync = new Map>() + async handleEvent(integration: IntegrationContainer, derivedClient: TxOperations, evt: T): Promise { + await this.createCommentPromise + const event = evt as PullRequestReviewThreadEvent + + if (event.sender.type === 'Bot') { + // Ignore events from Bot if it is our bot + // No need to handle event from ourself + if (event.sender.login.includes(config.BotName)) { + return + } + } + this.ctx.info('reviewThreads:handleEvent', { event, workspace: this.provider.getWorkspaceId().name }) + + const { project, repository } = await this.provider.getProjectAndRepository(event.repository.node_id) + if (project === undefined || repository === undefined) { + this.ctx.info('No project for repository', { + name: event.repository.name, + workspace: this.provider.getWorkspaceId().name + }) + return + } + + await this.eventSync.get(event.thread.node_id) + const promise = this.processEvent(event, derivedClient, repository, integration) + this.eventSync.set(event.thread.node_id, promise) + await promise + this.eventSync.delete(event.thread.node_id) + } + + async handleDelete ( + existing: Doc | undefined, + info: DocSyncInfo, + derivedClient: TxOperations, + deleteExisting: boolean + ): Promise { + const container = await this.provider.getContainer(info.space) + if (container === undefined) { + return false + } + if ( + container?.container === undefined || + ((container.project.projectNodeId === undefined || + !container.container.projectStructure.has(container.project._id)) && + syncConfig.MainProject) + ) { + return false + } + + const commentExternal = info.external + + if (commentExternal === undefined) { + // No external issue yet, safe delete, since platform document will be deleted a well. + return true + } + const account = + existing?.createdBy ?? (await this.provider.getAccountU(commentExternal.user))?._id ?? core.account.System + + if (commentExternal !== undefined) { + try { + await this.deleteGithubDocument(container, account, commentExternal.node_id) + } catch (err: any) { + let cnt = false + if (Array.isArray(err.errors)) { + for (const e of err.errors) { + if (e.type === 'NOT_FOUND') { + // Ok issue is already deleted + cnt = true + break + } + } + } + if (!cnt) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + await derivedClient.update(info, { error: errorToObj(err) }) + } + } + } + + if (existing !== undefined && deleteExisting) { + await deleteObjects(this.ctx, this.client, [existing], account) + } + return true + } + + async deleteGithubDocument (container: ContainerFocus, account: Ref, id: string): Promise { + // Not supported + } + + private async processEvent ( + event: PullRequestReviewThreadEvent, + derivedClient: TxOperations, + repo: GithubIntegrationRepository, + integration: IntegrationContainer + ): Promise { + const account = (await this.provider.getAccountU(event.sender))?._id ?? core.account.System + + let externalData: ReviewThreadExternalData + try { + const response: any = await integration.octokit?.graphql( + ` + query listReview($reviewID: ID!) { + node(id: $reviewID) { + ... on PullRequestReviewThread { + ${reviewThreadDetails} + } + } + } + `, + { + reviewID: event.thread.node_id + } + ) + externalData = response.node + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + return + } + if (externalData === undefined) { + return + } + + switch (event.action) { + case 'resolved': + case 'unresolved': { + const isResolved = event.action === 'resolved' + const reviewData = await this.client.findOne(github.class.DocSyncInfo, { url: event.thread.node_id }) + + if (reviewData !== undefined) { + const reviewObj: GithubReviewThread | undefined = await this.client.findOne( + reviewData.objectClass, + { + _id: reviewData._id as unknown as Ref + } + ) + if (reviewObj !== undefined) { + const lastModified = Date.now() + await derivedClient.diffUpdate( + reviewData, + { + external: externalData, + current: { ...reviewData.current, isResolved }, + needSync: githubSyncVersion, + lastModified + }, + lastModified + ) + await this.client.update( + reviewObj, + { + isResolved + }, + false, + lastModified, + account + ) + + // We need to trigger PR external update, to properly handle todos. + } + + const reviewPR = await this.client.findOne(github.class.DocSyncInfo, { + url: (reviewData.parent ?? '').toLowerCase() + }) + if (reviewPR !== undefined) { + await derivedClient.update(reviewPR, { + externalVersion: '' + }) + } + this.provider.sync() + } + break + } + } + } + + async sync ( + existing: Doc | undefined, + info: DocSyncInfo, + parent: DocSyncInfo | undefined, + derivedClient: TxOperations + ): Promise | undefined> { + const container = await this.provider.getContainer(info.space) + if (container?.container === undefined) { + return {} + } + if (parent === undefined) { + return { needSync: '' } + } + if (info.external === undefined) { + // TODO: Use selected repository + const repo = container.repository.find((it) => it._id === parent?.repository) + if (repo?.nodeId === undefined) { + // No need to sync if parent repository is not defined. + return { needSync: githubSyncVersion } + } + + // If no external document, we need to create it. + this.createCommentPromise = this.createGithubReviewThread(container, existing, info, parent, derivedClient) + return await this.createCommentPromise + } + const review = info.external as ReviewThreadExternalData + + // Use first comment as author, since github doesn't provide one. + const account = + existing?.modifiedBy ?? + (await this.provider.getAccount(review.comments.nodes[0].author ?? null))?._id ?? + core.account.System + + const messageData: ReviewThreadData = { + threadId: review.id, + diffSide: review.diffSide, + isCollapsed: review.isCollapsed, + isOutdated: review.isOutdated, + isResolved: review.isResolved, + line: review.line, + startLine: review.startLine, + originalLine: review.originalLine, + originalStartLine: review.originalStartLine, + path: review.path, + resolvedBy: (await this.provider.getAccount(review.resolvedBy))?._id ?? core.account.System, + startDiffSide: review.startDiffSide + } + if (existing === undefined) { + try { + await this.createReviewThread(info, messageData, parent, review, account) + return { needSync: githubSyncVersion, current: messageData } + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + return { needSync: githubSyncVersion, error: errorToObj(err) } + } + } else { + await this.handleDiffUpdate(existing, info, messageData, container, parent, review, account, derivedClient) + } + return { current: messageData, needSync: githubSyncVersion } + } + + private async handleDiffUpdate ( + existing: Doc, + info: DocSyncInfo, + reviewData: ReviewThreadData, + container: ContainerFocus, + parent: DocSyncInfo, + review: ReviewThreadExternalData, + account: Ref, + derivedClient: TxOperations + ): Promise { + const repository = container.repository.find((it) => it._id === info.repository) + if (repository === undefined) { + return + } + + const existingReview = existing as GithubReviewThread + + const previousData: ReviewThreadData = info.current ?? ({} as unknown as ReviewThreadData) + + const update = collectUpdate(previousData, reviewData, Object.keys(reviewData)) + + const platformUpdate = collectUpdate(previousData, existing, Object.keys(reviewData)) + + // We should remove changes we already have from github changed. + for (const [k, v] of Object.entries(update)) { + if ((platformUpdate as any)[k] !== v) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (platformUpdate as any)[k] + } + } + // Remove current same values from update + for (const [k, v] of Object.entries(existingReview)) { + if ((update as any)[k] === v) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (update as any)[k] + } + } + + if (Object.keys(platformUpdate).length > 0) { + // Check and update external + if (platformUpdate.isResolved !== undefined) { + const okit = (await this.provider.getOctokit(account as Ref)) ?? container.container.octokit + const q = `mutation updateReviewThread($threadID: ID!) { + ${platformUpdate.isResolved ? 'resolveReviewThread' : 'unresolveReviewThread'} ( + input: { + threadId: $threadID + }) { + thread { + id + isResolved + } + } + }` + try { + if (isGHWriteAllowed()) { + await okit?.graphql(q, { + threadID: review.id + }) + } + } catch (err: any) { + update.isResolved = !platformUpdate.isResolved + platformUpdate.isResolved = !platformUpdate.isResolved + this.ctx.error('Error', { err }) + Analytics.handleError(err) + } + await derivedClient.update(info, { external: { ...info.external, isResolved: platformUpdate.isResolved } }) + } + } + if (Object.keys(update).length > 0) { + await this.client.update(existing, update, false, getUpdatedAtReviewThread(review), account) + } + } + + private async createReviewThread ( + info: DocSyncInfo, + messageData: ReviewThreadData, + parent: DocSyncInfo, + review: ReviewThreadExternalData, + account: Ref + ): Promise { + const _id: Ref = info._id as unknown as Ref + const value: AttachedData = { + ...messageData + } + await this.client.addCollection( + github.class.GithubReviewThread, + info.space, + parent._id, + parent.objectClass, + 'activity', + value, + _id, + new Date(review.comments.nodes[0].createdAt ?? Date.now()).getTime(), + account + ) + } + + async createGithubReviewThread ( + container: ContainerFocus, + existing: Doc | undefined, + info: DocSyncInfo, + parent: DocSyncInfo, + derivedClient: TxOperations + ): Promise> { + // TODO: Use selected repository + const repo = container.repository.find((it) => it._id === parent?.repository) + if (repo?.nodeId === undefined) { + // No need to sync if parent repository is not defined. + return { needSync: githubSyncVersion } + } + + if (parent === undefined) { + return {} + } + const existingReview = existing as GithubReviewThread + const okit = + (await this.provider.getOctokit(existingReview.modifiedBy as Ref)) ?? container.container.octokit + + // No external version yet, create it. + // Will be added into pending state. + try { + // Will be created in pending state. + const q = `mutation addPullRequestReviewThread($prID: ID!, $body: String!) { + addPullRequestReviewThread(input:{ + pullRequestId: $prID, + path: "${existingReview.path}" + body: $body, + line: ${existingReview.line}, + side: LEFT, + startSide: LEFT, + }) { + pullRequestReview { + ${reviewThreadDetails} + } + } + }` + + if (isGHWriteAllowed()) { + const response: + | { + addPullRequestReviewThread: { + thread: ReviewThreadExternalData + } + } + | undefined = await okit?.graphql(q, { + prID: (parent.external as PullRequestExternalData).id, + body: EmptyMarkup // TODO: Need to replace with first comment on comment sync. + }) + + const reviewExternal = response?.addPullRequestReviewThread?.thread + + if (reviewExternal !== undefined) { + const upd: DocumentUpdate = { + url: reviewExternal.id, + external: reviewExternal, + current: existing, + repository: repo._id, + version: githubSyncVersion, + externalVersion: githubExternalSyncVersion + } + // We need to update in current promise, to prevent event changes. + await derivedClient.update(info, upd) + } + } + return {} + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + return { needSync: githubSyncVersion, error: errorToObj(err) } + } + } + + async externalSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + kind: ExternalSyncField, + syncDocs: DocSyncInfo[], + repo: GithubIntegrationRepository, + prj: GithubProject + ): Promise { + if (kind === 'externalVersion') { + // No need to perform external sync for review threads, so let's update marks + const tx = derivedClient.apply('review_threads_github') + for (const d of syncDocs) { + await tx.update(d, { externalVersion: githubExternalSyncVersion }) + } + await tx.commit() + this.provider.sync() + } else if (kind === 'derivedVersion') { + // We need to create comments. + // Find a pull request parents + + const allParents = syncDocs + .map((it) => (it.parent ?? '').toLowerCase()) + .filter((it, idx, arr) => it != null && arr.indexOf(it) === idx) + const parents = await derivedClient.findAll(github.class.DocSyncInfo, { + url: { + $in: allParents + } + }) + + for (const d of syncDocs) { + const ext = d.external as ReviewThreadExternalData + if (ext == null) { + continue + } + if (ext.comments.nodes.length < ext.comments.totalCount) { + // TODO: We need to fetch missing items. + } + + const prParent = parents.find((it) => it.url === d.parent?.toLowerCase()) + if (prParent === undefined) { + continue + } + await syncDerivedDocuments( + derivedClient, + prParent, + { ...ext, url: (d.parent ?? '').toLowerCase() }, // Parent is Pull request. + prj, + repo, + github.class.GithubReviewComment, + { + reviewThreadId: ext.id + }, + (ext) => ext.comments.nodes, + { reviewThreadId: ext.id } + ) + } + const tx = derivedClient.apply('reviewThread_github') + for (const d of syncDocs) { + await tx.update(d, { derivedVersion: githubDerivedSyncVersion }) + } + await tx.commit() + this.provider.sync() + } + } + + repositoryDisabled (integration: IntegrationContainer, repo: GithubIntegrationRepository): void {} + + async externalFullSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + projects: GithubProject[], + repositories: GithubIntegrationRepository[] + ): Promise { + // No external sync for reviews, they are done in pull requests. + } +} diff --git a/services/github/pod-github/src/sync/reviews.ts b/services/github/pod-github/src/sync/reviews.ts new file mode 100644 index 0000000000..e0d65c1658 --- /dev/null +++ b/services/github/pod-github/src/sync/reviews.ts @@ -0,0 +1,494 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +import { PersonAccount } from '@hcengineering/contact' +import core, { + Account, + AttachedData, + Doc, + DocumentUpdate, + MeasureContext, + Ref, + TxOperations +} from '@hcengineering/core' +import { LiveQuery } from '@hcengineering/query' +import github, { + DocSyncInfo, + GithubIntegrationRepository, + GithubProject, + GithubPullRequestReviewState, + GithubReview +} from '@hcengineering/github' +import { + ContainerFocus, + DocSyncManager, + ExternalSyncField, + IntegrationContainer, + IntegrationManager, + githubExternalSyncVersion, + githubSyncVersion +} from '../types' +import { PullRequestExternalData, Review as ReviewExternalData, reviewDetails, toReviewState } from './githubTypes' +import { collectUpdate, deleteObjects, errorToObj, isGHWriteAllowed } from './utils' + +import { Analytics } from '@hcengineering/analytics' +import { PullRequestReviewEvent, PullRequestReviewSubmittedEvent } from '@octokit/webhooks-types' +import config from '../config' +import { syncConfig } from './syncConfig' + +export type ReviewData = Pick + +export class ReviewSyncManager implements DocSyncManager { + provider!: IntegrationManager + + createCommentPromise: Promise> | undefined + + externalDerivedSync = false + + constructor ( + readonly ctx: MeasureContext, + readonly client: TxOperations, + readonly lq: LiveQuery + ) {} + + async init (provider: IntegrationManager): Promise { + this.provider = provider + } + + eventSync = new Map>() + async handleEvent(integration: IntegrationContainer, derivedClient: TxOperations, evt: T): Promise { + await this.createCommentPromise + const event = evt as PullRequestReviewEvent + + if (event.sender.type === 'Bot') { + // Ignore events from Bot if it is our bot + // No need to handle event from ourself + if (event.sender.login.includes(config.BotName)) { + return + } + } + this.ctx.info('reviews:handleEvent', { event, workspace: this.provider.getWorkspaceId().name }) + + const { project, repository } = await this.provider.getProjectAndRepository(event.repository.node_id) + if (project === undefined || repository === undefined) { + this.ctx.info('No project for repository', { + name: event.repository.name, + workspace: this.provider.getWorkspaceId().name + }) + return + } + + await this.eventSync.get(event.review.html_url) + const promise = this.processEvent(event, derivedClient, repository, integration) + this.eventSync.set(event.review.html_url, promise) + await promise + this.eventSync.delete(event.review.html_url) + } + + async handleDelete ( + existing: Doc | undefined, + info: DocSyncInfo, + derivedClient: TxOperations, + deleteExisting: boolean + ): Promise { + const container = await this.provider.getContainer(info.space) + if (container === undefined) { + return false + } + if ( + container?.container === undefined || + ((container.project.projectNodeId === undefined || + !container.container.projectStructure.has(container.project._id)) && + syncConfig.MainProject) + ) { + return false + } + + const commentExternal = info.external + + if (commentExternal === undefined) { + // No external issue yet, safe delete, since platform document will be deleted a well. + return true + } + const account = + existing?.createdBy ?? (await this.provider.getAccountU(commentExternal.user))?._id ?? core.account.System + + if (commentExternal !== undefined) { + try { + await this.deleteGithubDocument(container, account, commentExternal.node_id) + } catch (err: any) { + let cnt = false + if (Array.isArray(err.errors)) { + for (const e of err.errors) { + if (e.type === 'NOT_FOUND') { + // Ok issue is already deleted + cnt = true + break + } + } + } + if (!cnt) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + await derivedClient.update(info, { error: errorToObj(err) }) + } + } + } + + if (existing !== undefined && deleteExisting) { + await deleteObjects(this.ctx, this.client, [existing], account) + } + return true + } + + async deleteGithubDocument (container: ContainerFocus, account: Ref, id: string): Promise { + const okit = (await this.provider.getOctokit(account as Ref)) ?? container.container.octokit + const q = `mutation deleteReview($reviewID: ID!) { + deletePullRequestReview(input: { + pullRequestReviewId: $reviewID + }) { + pullRequestReview { + url + } + } + }` + if (isGHWriteAllowed()) { + await okit?.graphql(q, { + reviewID: id + }) + } + } + + private async processEvent ( + event: PullRequestReviewEvent, + derivedClient: TxOperations, + repo: GithubIntegrationRepository, + integration: IntegrationContainer + ): Promise { + const account = (await this.provider.getAccountU(event.sender))?._id ?? core.account.System + + let externalData: ReviewExternalData + try { + const response: any = await integration.octokit?.graphql( + ` + query listReview($reviewID: ID!) { + node(id: $reviewID) { + ... on PullRequestReview { + ${reviewDetails} + } + } + } + `, + { + reviewID: event.review.node_id + } + ) + externalData = response.node + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + return + } + if (externalData === undefined) { + return + } + + switch (event.action) { + case 'submitted': { + await this.createSyncData(event, derivedClient, repo, externalData) + + const parentDoc = await this.client.findOne(github.class.DocSyncInfo, { + url: (event.pull_request.html_url ?? '').toLowerCase() + }) + if (parentDoc !== undefined) { + await derivedClient.update(parentDoc, { + externalVersion: '', + derivedVersion: '' + }) + this.provider.sync() + } + break + } + case 'dismissed': { + const reviewData = await this.client.findOne(github.class.DocSyncInfo, { + url: (event.review.html_url ?? '').toLowerCase() + }) + + if (reviewData !== undefined) { + const reviewObj: GithubReview | undefined = await this.client.findOne(reviewData.objectClass, { + _id: reviewData._id as unknown as Ref + }) + if (reviewObj !== undefined) { + const lastModified = Date.now() + await derivedClient.diffUpdate( + reviewData, + { + external: externalData, + current: { ...reviewData.current, state: GithubPullRequestReviewState.Dismissed }, + needSync: githubSyncVersion, + lastModified + }, + lastModified + ) + await this.client.update( + reviewObj, + { + state: GithubPullRequestReviewState.Dismissed + }, + false, + lastModified, + account + ) + this.provider.sync() + } + } + break + } + } + } + + private async createSyncData ( + createdEvent: PullRequestReviewSubmittedEvent, + derivedClient: TxOperations, + repo: GithubIntegrationRepository, + externalData: ReviewExternalData + ): Promise { + const reviewData = await this.client.findOne(github.class.DocSyncInfo, { + url: (createdEvent.review.html_url ?? '').toLowerCase() + }) + + if (reviewData === undefined) { + await derivedClient.createDoc(github.class.DocSyncInfo, repo.githubProject as Ref, { + url: createdEvent.review.html_url.toLowerCase(), + needSync: '', + githubNumber: 0, + repository: repo._id, + objectClass: github.class.GithubReview, + external: externalData, + externalVersion: githubExternalSyncVersion, + derivedVersion: '', + parent: createdEvent.pull_request.html_url, + lastModified: new Date(createdEvent.review.submitted_at ?? Date.now()).getTime() + }) + } + } + + async sync ( + existing: Doc | undefined, + info: DocSyncInfo, + parent: DocSyncInfo | undefined, + derivedClient: TxOperations + ): Promise | undefined> { + const container = await this.provider.getContainer(info.space) + if (container?.container === undefined) { + return {} + } + if (parent === undefined) { + return { needSync: '' } + } + if (info.external === undefined) { + // TODO: Use selected repository + const repo = container.repository.find((it) => it._id === parent?.repository) + if (repo?.nodeId === undefined) { + // No need to sync if parent repository is not defined. + return { needSync: githubSyncVersion } + } + + // If no external document, we need to create it. + this.createCommentPromise = this.createGithubReview(container, existing, info, parent, derivedClient) + return await this.createCommentPromise + } + const review = info.external as ReviewExternalData + + const account = existing?.modifiedBy ?? (await this.provider.getAccount(review.author))?._id ?? core.account.System + + const messageData: ReviewData = { + body: await this.provider.getMarkup(container.container, review.body), + state: toReviewState(review.state), + comments: (review.comments?.nodes ?? []).map((it) => it.url) + } + if (existing === undefined) { + try { + await this.createReview(info, messageData, parent, review, account) + return { needSync: githubSyncVersion, current: messageData } + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + return { needSync: githubSyncVersion, error: errorToObj(err) } + } + } else { + await this.handleDiffUpdate(existing, info, messageData, container, parent, review, account) + } + return { current: messageData, needSync: githubSyncVersion } + } + + private async handleDiffUpdate ( + existing: Doc, + info: DocSyncInfo, + reviewData: ReviewData, + container: ContainerFocus, + parent: DocSyncInfo, + review: ReviewExternalData, + account: Ref + ): Promise { + const repository = container.repository.find((it) => it._id === info.repository) + if (repository === undefined) { + return + } + + const existingReview = existing as GithubReview + + const previousData: ReviewData = info.current ?? ({} as unknown as ReviewData) + + const update = collectUpdate(previousData, reviewData, Object.keys(reviewData)) + + const platformUpdate = collectUpdate(previousData, existing, Object.keys(reviewData)) + + // We should remove changes we already have from github changed. + for (const [k, v] of Object.entries(update)) { + if ((platformUpdate as any)[k] !== v) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (platformUpdate as any)[k] + } + } + // Remove current same values from update + for (const [k, v] of Object.entries(existingReview)) { + if ((update as any)[k] === v) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (update as any)[k] + } + } + + if (Object.keys(platformUpdate).length > 0) { + // Check and update body with external + // No update is possible for review. + } + if (Object.keys(update).length > 0) { + await this.client.update(existing, update, false, new Date(review.updatedAt ?? Date.now()).getTime(), account) + } + } + + private async createReview ( + info: DocSyncInfo, + messageData: ReviewData, + parent: DocSyncInfo, + review: ReviewExternalData, + account: Ref + ): Promise { + const _id: Ref = info._id as unknown as Ref + const value: AttachedData = { + ...messageData + } + await this.client.addCollection( + github.class.GithubReview, + info.space, + parent._id, + parent.objectClass, + 'activity', + value, + _id, + new Date(review.submittedAt ?? review.createdAt ?? Date.now()).getTime(), + account + ) + } + + async createGithubReview ( + container: ContainerFocus, + existing: Doc | undefined, + info: DocSyncInfo, + parent: DocSyncInfo, + derivedClient: TxOperations + ): Promise> { + // TODO: Use selected repository + const repo = container.repository.find((it) => it._id === parent?.repository) + if (repo?.nodeId === undefined) { + // No need to sync if parent repository is not defined. + return { needSync: githubSyncVersion } + } + + if (parent === undefined) { + return {} + } + const existingReview = existing as GithubReview + const okit = + (await this.provider.getOctokit(existingReview.modifiedBy as Ref)) ?? container.container.octokit + + // No external version yet, create it. + try { + // TOOD: Collect all threads and all pending comments to be added, and map them back. + const q = `mutation createReview($prID: ID!, $body: String!, $state: PullRequestReviewEvent!) { + addPullRequestReview(input:{ + pullRequestId: $prID, + body: $body, + event: $state + }) { + pullRequestReview { + ${reviewDetails} + } + } + }` + + if (isGHWriteAllowed()) { + const response: + | { + addPullRequestReview: { + pullRequestReview: ReviewExternalData + } + } + | undefined = await okit?.graphql(q, { + prID: (parent.external as PullRequestExternalData).id, + body: (await this.provider.getMarkdown(existingReview.body)) ?? '', + state: existingReview.state + }) + + const reviewExternal = response?.addPullRequestReview?.pullRequestReview + + if (reviewExternal !== undefined) { + const upd: DocumentUpdate = { + url: reviewExternal.url.toLowerCase(), + external: reviewExternal, + current: existing, + repository: repo._id, + version: githubSyncVersion, + externalVersion: githubExternalSyncVersion + } + // We need to update in current promise, to prevent event changes. + await derivedClient.update(info, upd) + } + } + return {} + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + return { needSync: githubSyncVersion, error: errorToObj(err) } + } + } + + async externalSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + kind: ExternalSyncField, + syncDocs: DocSyncInfo[], + repository: GithubIntegrationRepository, + project: GithubProject + ): Promise { + // No need to perform external sync for reviews, so let's update marks + const tx = derivedClient.apply('reviews_github') + for (const d of syncDocs) { + await tx.update(d, { externalVersion: githubExternalSyncVersion }) + } + await tx.commit() + this.provider.sync() + } + + repositoryDisabled (integration: IntegrationContainer, repo: GithubIntegrationRepository): void {} + + async externalFullSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + projects: GithubProject[], + repositories: GithubIntegrationRepository[] + ): Promise { + // No external sync for reviews, they are done in pull requests. + } +} diff --git a/services/github/pod-github/src/sync/syncConfig.ts b/services/github/pod-github/src/sync/syncConfig.ts new file mode 100644 index 0000000000..9da406f2b4 --- /dev/null +++ b/services/github/pod-github/src/sync/syncConfig.ts @@ -0,0 +1,10 @@ +/** + * @public + */ +export const syncConfig = { + MainProject: false, + SupportMilestones: true, + IssuesInProject: true, + BacklogInProject: false, + PullRequestsInProject: false +} diff --git a/services/github/pod-github/src/sync/users.ts b/services/github/pod-github/src/sync/users.ts new file mode 100644 index 0000000000..94f5d43b72 --- /dev/null +++ b/services/github/pod-github/src/sync/users.ts @@ -0,0 +1,268 @@ +import { Analytics } from '@hcengineering/analytics' +import { Doc, DocumentUpdate, MeasureContext, TxOperations } from '@hcengineering/core' +import { LiveQuery } from '@hcengineering/query' +import { DocSyncInfo, GithubIntegrationRepository, GithubProject } from '@hcengineering/github' +import { Octokit } from 'octokit' +import { DocSyncManager, ExternalSyncField, IntegrationContainer, IntegrationManager } from '../types' +import { UserInfo } from './githubTypes' + +export class UsersSyncManager implements DocSyncManager { + provider!: IntegrationManager + + constructor ( + readonly ctx: MeasureContext, + readonly client: TxOperations, + readonly lq: LiveQuery + ) {} + + externalDerivedSync = false + + async init (provider: IntegrationManager): Promise { + this.provider = provider + } + + async sync ( + existing: Doc | undefined, + info: DocSyncInfo, + parent?: DocSyncInfo + ): Promise | undefined> { + return {} + } + + async handleDelete ( + existing: Doc | undefined, + info: DocSyncInfo, + derivedClient: TxOperations, + deleteExisting: boolean + ): Promise { + return false + } + + async handleEvent(integration: IntegrationContainer, derivedClient: TxOperations, evt: T): Promise {} + + async externalSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + kind: ExternalSyncField, + syncDocs: DocSyncInfo[], + repo: GithubIntegrationRepository, + prj: GithubProject + ): Promise {} + + repositoryDisabled (integration: IntegrationContainer, repo: GithubIntegrationRepository): void { + integration.synchronized.delete(`${repo._id}:users`) + } + + async externalFullSync ( + integration: IntegrationContainer, + derivedClient: TxOperations, + projects: GithubProject[], + repositories: GithubIntegrationRepository[] + ): Promise { + for (const repo of repositories) { + const syncKey = `${repo._id}:users` + if ( + repo.githubProject === undefined || + !repo.enabled || + integration.synchronized.has(syncKey) || + integration.octokit === undefined || + repo.nodeId === undefined + ) { + continue + } + + await this.syncUsers('assignableUsers', integration, repo) + await this.syncUsers('mentionableUsers', integration, repo) + + integration.synchronized.add(syncKey) + } + } + + async syncUsers (key: string, integration: IntegrationContainer, repo: GithubIntegrationRepository): Promise { + const assignableUsersIterator = integration.octokit.graphql.paginate.iterator( + `query listUsers($name: String!, $owner: String!, $cursor: String) { + repository(name: $name, owner: $owner) { + ${key}(first: 50, after: $cursor) { + nodes { + id + email + avatarUrl + login + name + } + pageInfo { + startCursor + hasNextPage + endCursor + } + totalCount + } + } + } + `, + { + name: repo.name, + owner: repo.owner?.login ?? '' + } + ) + try { + for await (const data of assignableUsersIterator) { + const users: UserInfo[] = data.repository[key]?.nodes ?? [] + for (const d of users) { + if (d.login !== undefined) { + try { + await this.provider.getAccount(d) + continue + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + } + } + } + } + } catch (err: any) { + this.ctx.error('Error', { err }) + Analytics.handleError(err) + } + } +} + +export async function fetchViewerDetails (okit: Octokit): Promise<{ + viewer: { + followers: { + totalCount: number + } + following: { + totalCount: number + } + repositories: { + totalCount: number + } + openIssues: { + totalCount: number + } + closedIssues: { + totalCount: number + } + openPRs: { + totalCount: number + } + mergedPRs: { + totalCount: number + } + closedPRs: { + totalCount: number + } + repositoryDiscussions: { + totalCount: number + } + repositoriesContributedTo: { + totalCount: number + } + starredRepositories: { + totalCount: number + } + + id: string + login: string + email: string | undefined + url: string | undefined + name: string | undefined + bio: string | undefined + location: string | undefined + company: string | undefined + avatarUrl: string | undefined + createdAt: string | undefined + updatedAt: string | undefined + organizations: { + totalCount: number + nodes: { + url: string + avatarUrl: string | undefined + name: string | undefined + description: string | undefined + archivedAt: string | undefined + email: string | undefined + viewerIsAMember: boolean + updatedAt: string | undefined + resourcePath: string | undefined + descriptionHTML: string | undefined + location: string | undefined + websiteUrl: string | undefined + }[] + } + } +}> { + const request = ` + { + viewer { + followers(first:0) { + totalCount + } + following(first:0) { + totalCount + } + repositories(first:0) { + totalCount + } + openIssues:issues(first:0, states:[OPEN]) { + totalCount + } + closedIssues:issues (first:0, states:[CLOSED]) { + totalCount + } + + openPRs: pullRequests(first:0, states:OPEN) { + totalCount + } + mergedPRs: pullRequests(first:0, states:MERGED) { + totalCount + } + closedPRs: pullRequests(first:0, states:CLOSED) { + totalCount + } + repositoryDiscussions(first:0) { + totalCount + } + repositoriesContributedTo(first:0) { + totalCount + } + starredRepositories(first:0) { + totalCount + } + + id + login + email + url + name + bio + location + company + avatarUrl + createdAt + updatedAt + organizations(first: 50) { + totalCount + nodes { + url + avatarUrl + name + resourcePath + description + archivedAt + email + viewerIsAMember + archivedAt + updatedAt + description + descriptionHTML + location + websiteUrl + } + } + } + } + ` + return await okit.graphql(request) +} diff --git a/services/github/pod-github/src/sync/utils.ts b/services/github/pod-github/src/sync/utils.ts new file mode 100644 index 0000000000..bd07305c55 --- /dev/null +++ b/services/github/pod-github/src/sync/utils.ts @@ -0,0 +1,413 @@ +import { Analytics } from '@hcengineering/analytics' +import core, { + Account, + AnyAttribute, + AttachedDoc, + Class, + Doc, + DocumentQuery, + DocumentUpdate, + MeasureContext, + Ref, + SortingOrder, + Status, + Timestamp, + TxOperations, + Type, + toIdMap +} from '@hcengineering/core' +import { PlatformError, unknownStatus } from '@hcengineering/platform' +import task, { TaskType, calculateStatuses, createState, findStatusAttr } from '@hcengineering/task' +import tracker, { IssueStatus } from '@hcengineering/tracker' +import github, { + DocSyncInfo, + GithubIntegrationRepository, + GithubIssueStateReason, + GithubProject +} from '@hcengineering/github' +import { deepEqual } from 'fast-equals' +import { IntegrationManager, githubExternalSyncVersion } from '../types' +import { GithubDataType } from './githubTypes' + +/** + * Return if github write operations are allowed. + */ +export function isGHWriteAllowed (): boolean { + if (process.env.GITHUB_READONLY === 'true') { + return false + } + return true +} + +/** + * @public + */ +export function collectUpdate ( + doc: Record, + newDoc: Record, + keys: string[] +): DocumentUpdate { + const documentUpdate: DocumentUpdate = {} + function toUndefinedValues (a: any): any { + if (typeof a === 'object' && a != null) { + const newA: any = {} + for (const [k, v] of Object.entries(a)) { + if (v === null) { + newA[k] = undefined + } else { + newA[k] = toUndefinedValues(v) + } + } + return newA + } + return a ?? undefined + } + for (const k of keys) { + const v = newDoc[k] + if (!keys.includes(k)) { + continue + } + if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) { + continue + } + let vv = v + if (vv === undefined) { + vv = null + } + const dv = (doc as any)[k] + if (!deepEqual(toUndefinedValues(dv), toUndefinedValues(v))) { + ;(documentUpdate as any)[k] = vv + } + } + return documentUpdate as DocumentUpdate +} + +/** + * @public + */ +export async function getSince ( + _client: TxOperations, + _class: Ref>, + repo: GithubIntegrationRepository +): Promise { + const lastModified: Timestamp | undefined = await getSinceRaw(_client, _class, repo) + return lastModified !== undefined ? new Date(lastModified + 1)?.toISOString() : undefined +} + +/** + * @public + */ +export async function getSinceRaw ( + _client: TxOperations, + _class: Ref>, + repo: GithubIntegrationRepository +): Promise { + if (repo.githubProject == null) { + return undefined + } + return ( + await _client.findOne( + github.class.DocSyncInfo, + { + objectClass: _class, + space: repo.githubProject, + lastModified: { $exists: true }, + externalVersion: githubExternalSyncVersion, + externalVersionSince: { $ne: '#' }, + repository: repo._id + }, + { sort: { lastModified: SortingOrder.Descending }, limit: 1 } + ) + )?.lastModified +} + +/** + * @public + */ +export function gqlp (params: Record): string { + let result = '' + let first = true + for (const [k, v] of Object.entries(params)) { + if (v !== undefined) { + if (!first) { + result += ', ' + } + first = false + if (typeof v === 'number') { + result += `${k}: ${v}` + } else if (Array.isArray(v)) { + result += `${k}: [${v.map((it) => `"${it}"`).join(', ')}]` + } else { + result += `${k}: "${v}"` + } + } + } + return result +} + +/** + * @public + */ +export async function getCreateStatus ( + ctx: MeasureContext, + provider: IntegrationManager, + client: TxOperations, + prj: GithubProject, + name: string, + description: string, + colorStr: string, + taskType: TaskType +): Promise { + const color = hashCode(colorStr) + + const states = await provider.getStatuses(taskType._id) + + for (const s of states) { + if (s.name.toLowerCase().trim() === name.toLowerCase().trim()) { + return s._id + } + } + ctx.error('Create new project Status', { name, colorStr, category: 'Backlog' }) + // No status found, let's create one. + const id = await createState(client, taskType.statusClass, { + name, + description, + color, + ofAttribute: findStatusAttr(client.getHierarchy(), taskType.statusClass)._id, + category: task.statusCategory.UnStarted + }) + const type = await client.findOne(task.class.ProjectType, { _id: prj.type }) + if (type === undefined) { + return id + } + + if (!taskType.statuses.includes(id)) { + await client.update(taskType, { + $push: { statuses: id } + }) + const taskTypes = toIdMap(await client.findAll(task.class.TaskType, { parent: type._id })) + + const index = type.statuses.findIndex((it) => it._id === id) + if (index === -1) { + await client.update(type, { + statuses: calculateStatuses(type, taskTypes, [{ taskTypeId: taskType._id, statuses: taskType.statuses }]) + }) + } + } + return id +} + +/** + * @public + */ +export function hashCode (str: string): number { + return str.split('').reduce((prevHash, currVal) => ((prevHash << 5) - prevHash + currVal.charCodeAt(0)) | 0, 0) +} + +export function getType (attr: AnyAttribute): GithubDataType | undefined { + if (attr.type._class === core.class.TypeString) { + return 'TEXT' + } + if ( + attr.type._class === core.class.TypeNumber || + attr.type._class === tracker.class.TypeReportedTime || + attr.type._class === tracker.class.TypeEstimation || + attr.type._class === tracker.class.TypeRemainingTime + ) { + return 'NUMBER' + } + if (attr.type._class === core.class.TypeDate) { + return 'DATE' + } + if (attr.type._class === core.class.EnumOf) { + return 'SINGLE_SELECT' + } +} + +export function getPlatformType (dataType: GithubDataType): Ref>> | undefined { + switch (dataType) { + case 'TEXT': + return core.class.TypeString + case 'NUMBER': + return core.class.TypeNumber + case 'DATE': + return core.class.TypeDate + case 'SINGLE_SELECT': + return core.class.EnumOf + } +} + +export async function guessStatus ( + pr: { state: 'OPEN' | 'CLOSED' | 'MERGED', stateReason?: GithubIssueStateReason | null }, + statuses: Status[] +): Promise { + const unstarted = (): Status | undefined => statuses.find((it) => it.category === task.statusCategory.UnStarted) + + const todo = (): Status | undefined => statuses.find((it) => it.category === task.statusCategory.ToDo) + const active = (): Status | undefined => statuses.find((it) => it.category === task.statusCategory.Active) + + const canceled = (): Status | undefined => statuses.find((it) => it.category === task.statusCategory.Lost) + const completed = (): Status | undefined => statuses.find((it) => it.category === task.statusCategory.Won) + + let result: IssueStatus | undefined + + if (pr.state === 'OPEN' && pr.stateReason == null) { + result = unstarted() ?? todo() ?? active() + } else if (pr.state === 'OPEN' && pr.stateReason === GithubIssueStateReason.Reopened) { + result = active() + } else if (pr.state === 'CLOSED' && pr.stateReason === GithubIssueStateReason.NotPlanned) { + result = canceled() + } else if (pr.state === 'CLOSED' || pr.state === 'MERGED') { + result = completed() + } else { + // By default put into backlog + result = unstarted() ?? todo() ?? active() + } + if (result === undefined) { + throw new PlatformError(unknownStatus(`No status found for GH issue status ${pr.state} ${pr.stateReason}`)) + } + return result +} + +/** + * @public + */ +export class SyncRunner { + eventSync = new Map>() + + async exec(id: string, op: () => Promise): Promise { + await this.eventSync.get(id) + const promise = op() + this.eventSync.set( + id, + promise.then(() => {}) + ) + const result = await promise + this.eventSync.delete(id) + return result + } +} + +/** + * @public + */ +export const syncRunner = new SyncRunner() + +export async function deleteObjects ( + ctx: MeasureContext, + client: TxOperations, + objects: Doc[], + account: Ref +): Promise { + const ops = client.apply('delete') + for (const object of objects) { + if (client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) { + const adoc = object as AttachedDoc + await ops + .removeCollection( + object._class, + object.space, + adoc._id, + adoc.attachedTo, + adoc.attachedToClass, + adoc.collection, + Date.now(), + account + ) + .catch((err) => { + Analytics.handleError(err) + ctx.error('filed to remove collection', err) + }) + } else { + await ops.removeDoc(object._class, object.space, object._id, Date.now(), account).catch((err) => { + Analytics.handleError(err) + ctx.error('filed to remove doc', err) + }) + } + } + await ops.commit() +} + +export async function syncDerivedDocuments ( + derivedClient: TxOperations, + parentDoc: DocSyncInfo, + ext: T, + prj: GithubProject, + repo: GithubIntegrationRepository, + objectClass: Ref>, + query: DocumentQuery, + docs: (ext: T) => { url: string, updatedAt: string | null, createdAt: string }[], + extra?: any +): Promise { + const childDocsOfClass = await derivedClient.findAll(github.class.DocSyncInfo, { + objectClass, + parent: (parentDoc.url ?? '').toLowerCase(), + ...query + }) + + const processed = new Set>() + const _docs = docs(ext) + for (const r of _docs) { + const existing = childDocsOfClass.find((it) => it.url.toLowerCase() === r.url.toLowerCase()) + if (existing === undefined) { + await derivedClient.createDoc(github.class.DocSyncInfo, prj._id, { + objectClass, + url: r.url.toLowerCase(), + needSync: '', // we need to sync to retrieve patch in background + githubNumber: 0, + repository: repo._id, + external: r, + externalVersion: githubExternalSyncVersion, + derivedVersion: '', + lastModified: new Date(r.updatedAt ?? r.createdAt).getTime(), + parent: ext.url, + attachedTo: parentDoc._id, + ...extra + }) + } else { + processed.add(existing._id) + if (!deepEqual(existing.external, r)) { + // Only update if had changes. + await derivedClient.update(existing, { + external: r, + needSync: '', // We need to check if we had any changes. + derivedVersion: '', + externalVersion: githubExternalSyncVersion, + lastModified: new Date(r.updatedAt ?? r.createdAt).getTime(), + ...extra + }) + } + } + } + + // Mark all non processed for delete. + for (const d of childDocsOfClass.filter((it) => !processed.has(it._id))) { + await derivedClient.update(d, { deleted: true, needSync: '' }) + } +} + +const errorPrinter = ({ message, stack, ...rest }: Error): object => ({ + message, + stack, + ...rest +}) +export function errorToObj (value: any): any { + return value instanceof Error ? errorPrinter(value) : value +} + +export function compareMarkdown (a: string, b: string): boolean { + let na = a.replaceAll('\r\n', '\n').replaceAll('\r', '\n') + let nb = b.replaceAll('\r\n', '\n').replaceAll('\r', '\n') + + // Remove trailings before compare + na = na + .split('\n') + .map((it) => it.trimEnd()) + .join('\n') + nb = nb + .split('\n') + .map((it) => it.trimEnd()) + .join('\n') + + return na === nb +} diff --git a/services/github/pod-github/src/types.ts b/services/github/pod-github/src/types.ts new file mode 100644 index 0000000000..ff2e730c40 --- /dev/null +++ b/services/github/pod-github/src/types.ts @@ -0,0 +1,208 @@ +import { Person, PersonAccount } from '@hcengineering/contact' +import { + Account, + Branding, + Class, + Data, + Doc, + DocumentUpdate, + Ref, + Space, + Status, + TxOperations, + WithLookup, + WorkspaceIdWithUrl, + type Blob +} from '@hcengineering/core' +import { LiveQuery } from '@hcengineering/query' +import { ProjectType, TaskType } from '@hcengineering/task' +import { MarkupNode } from '@hcengineering/text' +import { User } from '@octokit/webhooks-types' +import { + DocSyncInfo, + GithubIntegration, + GithubIntegrationRepository, + GithubMilestone, + GithubProject, + GithubUserInfo +} from '@hcengineering/github' +import { Octokit } from 'octokit' +import { GithubProjectV2 } from './sync/githubTypes' + +/** + * @public + */ +export const githubSyncVersion = 'v7' + +/** + * @public + */ +export const githubExternalSyncVersion = 'v4' + +/** + * @public + */ +export const githubDerivedSyncVersion = 'v1' + +/** + * @public + */ +export interface Workspace { + _id: string + workspace: string + productId: string +} + +/** + * @public + */ +export interface IntegrationContainer { + integration: WithLookup + installationId: number + installationName: string + octokit: Octokit + + projectStructure: Map, GithubProjectV2> + + enabled: boolean + synchronized: Set + + login: string + loginNodeId: string + type: GithubIntegration['type'] + + syncLock: Map, Promise> +} + +export type UserInfo = Data + +export interface ContainerFocus { + container: IntegrationContainer + repository: GithubIntegrationRepository[] + project: GithubProject +} + +export interface IntegrationManager { + liveQuery: LiveQuery + getContainer: (space: Ref) => Promise + getAccount: (user?: UserInfo | null) => Promise + getAccountU: (user: User) => Promise + getOctokit: (account: Ref) => Promise + getMarkup: ( + container: IntegrationContainer, + text?: string | null, + preprocessor?: (nodes: MarkupNode) => Promise + ) => Promise + getMarkdown: (text?: string | null, preprocessor?: (nodes: MarkupNode) => Promise) => Promise + sync: () => void + getGithubLogin: (container: IntegrationContainer, account: Ref) => Promise + + uploadFile: (patch: string, file?: string) => Promise + + getStatuses: (type: Ref | undefined) => Promise + getProjectStatuses: (type: Ref | undefined) => Promise + getProjectType: (type: Ref) => Promise + getTaskType: (type: Ref) => Promise + + getTaskTypeOf: (project: Ref, ofClass: Ref>) => Promise + + handleEvent: ( + requestClass: Ref>, + integrationId: number | undefined, + repo: GithubIntegrationRepository, + event: T + ) => Promise + + doSyncFor: (docs: DocSyncInfo[]) => Promise + getWorkspaceId: () => WorkspaceIdWithUrl + getBranding: () => Branding | null + + getProjectAndRepository: ( + repositoryId: string + ) => Promise<{ project?: GithubProject, repository?: GithubIntegrationRepository }> + + checkMarkdownConversion: ( + container: IntegrationContainer, + body: string + ) => Promise<{ markdownCompatible: boolean, markdown: string }> + + isPlatformUser: (account: Ref) => Promise +} + +export type ExternalSyncField = 'externalVersion' | 'derivedVersion' + +/** + * @public + * + * Will perform synchronization of document and external document. + */ +export interface DocSyncManager { + // Initialize the mapper. + init: (provider: IntegrationManager) => Promise + // Perform synchronization of document with external source. + sync: ( + existing: Doc | undefined, + info: DocSyncInfo, + parent: DocSyncInfo | undefined, + derivedClient: TxOperations + ) => Promise | undefined> + + handleDelete: ( + existing: Doc | undefined, + info: DocSyncInfo, + derivedClient: TxOperations, + deleteExisting: boolean, + parent?: DocSyncInfo + ) => Promise + + // Perform synchronization with external source. + externalFullSync: ( + integration: IntegrationContainer, + derivedClient: TxOperations, + projects: GithubProject[], + repositories: GithubIntegrationRepository[] + ) => Promise + + // Perform synchronization with external source. + externalSync: ( + integration: IntegrationContainer, + derivedClient: TxOperations, + kind: ExternalSyncField, + syncDocs: DocSyncInfo[], + repository: GithubIntegrationRepository, + project: GithubProject + ) => Promise + + handleEvent: (integration: IntegrationContainer, derivedClient: TxOperations, event: T) => Promise + + externalDerivedSync: boolean + + repositoryDisabled: (integration: IntegrationContainer, repo: GithubIntegrationRepository) => void +} + +/** + * @public + */ +export interface GithubIntegrationRecord { + installationId: number + workspace: string + accountId: Ref +} + +/** + * @public + */ +export interface GithubUserRecord { + _id: string // login + code?: string | null + token?: string + expiresIn?: number | null // seconds + refreshToken?: string | null + refreshTokenExpiresIn?: number | null + authorized?: boolean + state?: string + scope?: string + error?: string | null + + accounts: Record> +} diff --git a/services/github/pod-github/src/utils.ts b/services/github/pod-github/src/utils.ts new file mode 100644 index 0000000000..25cabca28c --- /dev/null +++ b/services/github/pod-github/src/utils.ts @@ -0,0 +1,31 @@ +import { Doc } from '@hcengineering/core' +import { deepEqual } from 'fast-equals' + +export function equalExceptKeys (a: T | T[], b: T | T[], keys: (keyof T)[]): boolean { + function equal (a: T, b: T): boolean { + const _a = { ...a } + const _b = { ...b } + for (const key of keys) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + if (key in _a) delete _a[key] + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + if (key in _b) delete _b[key] + } + return deepEqual(_a, _b) + } + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) { + return false + } + for (let i = 0; i < a.length; i++) { + if (!equal(a[i], b[i])) { + return false + } + } + return true + } + if (!Array.isArray(a) && !Array.isArray(b)) { + return equal(a, b) + } + return false +} diff --git a/services/github/pod-github/src/worker.ts b/services/github/pod-github/src/worker.ts new file mode 100644 index 0000000000..dc6c4f1a74 --- /dev/null +++ b/services/github/pod-github/src/worker.ts @@ -0,0 +1,1558 @@ +import { Analytics } from '@hcengineering/analytics' +import chunter from '@hcengineering/chunter' +import { CollaboratorClient } from '@hcengineering/collaborator-client' +import contact, { AvatarType, Person, PersonAccount } from '@hcengineering/contact' +import core, { + Account, + AccountRole, + AttachedDoc, + Branding, + Class, + Client, + ClientConnectEvent, + Data, + Doc, + DocumentQuery, + DocumentUpdate, + FindResult, + MeasureContext, + Ref, + SortingOrder, + Space, + Status, + Tx, + TxApplyIf, + TxCUD, + TxOperations, + TxWorkspaceEvent, + WithLookup, + WorkspaceEvent, + WorkspaceIdWithUrl, + concatLink, + generateId, + groupByArray, + toIdMap, + type Blob, + type MigrationState +} from '@hcengineering/core' +import { LiveQuery } from '@hcengineering/query' +import { StorageAdapter } from '@hcengineering/server-core' +import { getPublicLinkUrl } from '@hcengineering/server-guest-resources' +import task, { ProjectType, TaskType } from '@hcengineering/task' +import { MarkupNode, jsonToMarkup, isMarkdownsEquals } from '@hcengineering/text' +import tracker from '@hcengineering/tracker' +import { User } from '@octokit/webhooks-types' +import github, { + DocSyncInfo, + GithubAuthentication, + GithubIntegration, + GithubIntegrationRepository, + GithubIssue, + GithubMilestone, + GithubProject, + GithubUserInfo, + githubId +} from '@hcengineering/github' +import { App, Octokit } from 'octokit' +import { createPlatformClient } from './client' +import { createCollaboratorClient } from './collaborator' +import config from './config' +import { markupToMarkdown, parseMessageMarkdown } from './markdown' +import { createNotification } from './notifications' +import { InstallationRecord, PlatformWorker } from './platform' +import { CommentSyncManager } from './sync/comments' +import { IssueSyncManager } from './sync/issues' +import { ProjectsSyncManager } from './sync/projects' +import { PullRequestSyncManager } from './sync/pullrequests' +import { RepositorySyncMapper } from './sync/repository' +import { ReviewCommentSyncManager } from './sync/reviewComments' +import { ReviewThreadSyncManager } from './sync/reviewThreads' +import { ReviewSyncManager } from './sync/reviews' +import { syncConfig } from './sync/syncConfig' +import { UsersSyncManager, fetchViewerDetails } from './sync/users' +import { errorToObj } from './sync/utils' +import { + ContainerFocus, + DocSyncManager, + ExternalSyncField, + GithubUserRecord, + IntegrationContainer, + IntegrationManager, + UserInfo, + githubDerivedSyncVersion, + githubExternalSyncVersion, + githubSyncVersion +} from './types' +import { equalExceptKeys } from './utils' +/** + * @public + */ +export class GithubWorker implements IntegrationManager { + integrations: Map = new Map() + _client: TxOperations + closing: boolean = false + syncPromise?: Promise + + triggerRequests: number = 0 + + triggerSync: () => void = () => { + this.triggerRequests++ + } + + updateRequests: number = 0 + triggerUpdate: () => void = () => { + this.updateRequests++ + } + + mappers: { _class: Ref>[], mapper: DocSyncManager }[] = [] + + liveQuery: LiveQuery + + repositoryManager: RepositorySyncMapper + + collaborator: CollaboratorClient + + periodicTimer: any + + personMapper: UsersSyncManager + + async close (): Promise { + clearInterval(this.periodicTimer) + + this.closing = true + await this.syncPromise + await this.client.close() + } + + async refreshClient (clean: boolean): Promise { + await this.liveQuery.refreshConnect(clean) + } + + getWorkspaceId (): WorkspaceIdWithUrl { + return this.workspace + } + + getBranding (): Branding | null { + return this.branding + } + + async reloadRepositories (installationId: number): Promise { + const current = this.integrations.get(installationId) + if (current !== undefined) { + await this.repositoryManager.reloadRepositories(current) + this.triggerUpdate() + } + } + + async checkMarkdownConversion ( + container: IntegrationContainer, + body: string + ): Promise<{ markdownCompatible: boolean, markdown: string }> { + const markupText = await this.getMarkup(container, body) + const markDown = await this.getMarkdown(markupText) + return { markdownCompatible: isMarkdownsEquals(body, markDown), markdown: markDown } + } + + async getMarkup ( + container: IntegrationContainer, + text?: string | null, + preprocessor?: (nodes: MarkupNode) => Promise + ): Promise { + if (text == null) { + return '' + } + const frontUrl = this.getBranding()?.front ?? config.FrontURL + const refUrl = concatLink(frontUrl, `/browse/?workspace=${this.workspace.name}`) + const imageUrl = concatLink(frontUrl ?? config.FrontURL, `/files?workspace=${this.workspace.name}&file=`) + const guestUrl = getPublicLinkUrl(this.workspace, frontUrl) + const json = parseMessageMarkdown(text ?? '', refUrl, imageUrl, guestUrl) + await preprocessor?.(json) + return jsonToMarkup(json) + } + + async getMarkdown (text?: string | null, preprocessor?: (nodes: MarkupNode) => Promise): Promise { + if (text == null) { + return '' + } + return await markupToMarkdown( + text ?? '', + concatLink(this.getBranding()?.front ?? config.FrontURL, `/browse/?workspace=${this.workspace.name}`), + concatLink(this.getBranding()?.front ?? config.FrontURL, `/files?workspace=${this.workspace.name}&file=`), + preprocessor + ) + } + + async getContainer (space: Ref): Promise { + for (const v of this.integrations.values()) { + if (v.octokit === undefined) { + continue + } + const project = await this.liveQuery.findOne(github.mixin.GithubProject, { + _id: space as Ref + }) + if (project !== undefined) { + const repositories = await this.liveQuery.queryFind( + github.class.GithubIntegrationRepository, + {} + ) + return { + container: v, + repository: repositories.filter((it) => it.githubProject === space), + project + } + } + } + } + + async getAccountU (user: User): Promise { + return await this.getAccount({ + id: user.node_id, + login: user.login, + avatarUrl: user.avatar_url, + email: user.email ?? undefined, + name: user.name + }) + } + + accountMap = new Map>() + async getAccount (userInfo?: UserInfo | null): Promise { + if (userInfo?.login == null) { + return + } + const info = this.accountMap.get(userInfo?.login ?? '') + if (info !== undefined) { + return await info + } + const p = this._getAccountRaw(userInfo) + this.accountMap.set(userInfo?.login ?? '', p) + return await p + } + + async _getAccountRaw (userInfo?: UserInfo | null): Promise { + // We need to sync by userInfo id to prevent parallel requests. + if (userInfo === null) { + // Ghost author. + return await this.getAccount({ + id: 'ghost', + login: 'ghost', + avatarUrl: 'https://avatars.githubusercontent.com/u/10137?v=4', + email: '', + name: 'Ghost' + }) + } + if (userInfo?.login == null) { + return + } + const userName = (userInfo.name ?? userInfo.login) + .split(' ') + .map((it) => it.trim()) + .reverse() + .join(',') // TODO: Convert first, last name + + const infos = await this.liveQuery.findOne(github.class.GithubUserInfo, { login: userInfo.login }) + if (infos === undefined) { + await this._client.createDoc(github.class.GithubUserInfo, contact.space.Contacts, { + ...userInfo + }) + } + + const account = await this.liveQuery.findOne(contact.class.PersonAccount, { email: `github:${userInfo.login}` }) + if (account !== undefined) { + const person = await this.liveQuery.findOne(contact.class.Person, { _id: account.person }) + // We need to be sure employee are exists. + if (person === undefined) { + const person: Ref = await this.findPerson(userInfo, userName) + if (account.person !== person) { + await this._client.update(account, { person }) + } + } + return account + } else { + // Check authorized users + const accountRecord = await this.platform.getAccount(userInfo.login) + if (accountRecord !== undefined) { + const authorizedId = accountRecord.accounts[this.workspace.name] + if (authorizedId !== undefined) { + const emp = await this._client.findOne(contact.class.PersonAccount, { + _id: authorizedId as Ref + }) + if (emp !== undefined) { + // We need to create github account + const gid = await this._client.createDoc(contact.class.PersonAccount, core.space.Model, { + email: `github:${userInfo.login}`, + person: emp.person, + role: AccountRole.User + }) + const acc = await this._client.findOne(contact.class.PersonAccount, { _id: gid }) + return acc + } + } + } + + const person: Ref | undefined = await this.findPerson(userInfo, userName) + + // We need to create email account + const id = await this._client.createDoc(contact.class.PersonAccount, core.space.Model, { + email: `github:${userInfo.login}`, + person, + role: AccountRole.User + }) + const acc = await this._client.findOne(contact.class.PersonAccount, { _id: id }) + return acc + } + } + + private constructor ( + readonly ctx: MeasureContext, + readonly platform: PlatformWorker, + readonly installations: Map, + readonly client: Client, + readonly app: App, + readonly storageAdapter: StorageAdapter, + readonly workspace: WorkspaceIdWithUrl, + readonly branding: Branding | null, + readonly periodicSyncInterval = 60 * 60 * 1000 + ) { + this._client = new TxOperations(this.client, core.account.System) + this.liveQuery = new LiveQuery(client) + + this.repositoryManager = new RepositorySyncMapper(this.ctx.newChild('repository', {}), this._client, this.app) + + this.collaborator = createCollaboratorClient(this._client.getHierarchy(), this.workspace) + + this.personMapper = new UsersSyncManager(this.ctx.newChild('users', {}), this._client, this.liveQuery) + + this.mappers = [ + { _class: [github.mixin.GithubProject], mapper: this.repositoryManager }, + { + _class: [github.class.GithubIntegration, tracker.class.Milestone], + mapper: new ProjectsSyncManager(this.ctx.newChild('project', {}), this._client, this.liveQuery) + }, + { + _class: [tracker.class.Issue], + mapper: new IssueSyncManager(this.ctx.newChild('issue', {}), this._client, this.liveQuery, this.collaborator) + }, + { + _class: [github.class.GithubPullRequest], + mapper: new PullRequestSyncManager( + this.ctx.newChild('pullRequest', {}), + this._client, + this.liveQuery, + this.collaborator + ) + }, + { + _class: [chunter.class.ChatMessage], + mapper: new CommentSyncManager(this.ctx.newChild('comment', {}), this._client, this.liveQuery) + }, + { + _class: [contact.class.PersonAccount], + mapper: this.personMapper + }, + { + _class: [github.class.GithubReview], + mapper: new ReviewSyncManager(this.ctx.newChild('review', {}), this._client, this.liveQuery) + }, + { + _class: [github.class.GithubReviewThread], + mapper: new ReviewThreadSyncManager(this.ctx.newChild('review-thread', {}), this._client, this.liveQuery) + }, + { + _class: [github.class.GithubReviewComment], + mapper: new ReviewCommentSyncManager(this.ctx.newChild('review-comment', {}), this._client, this.liveQuery) + } + ] + + // We need to perform some periodic syncs, like sync users available, sync repo data. + this.periodicTimer = setInterval(() => { + if (this.periodicSyncPromise === undefined) { + this.periodicSyncPromise = this.performPeriodicSync() + } + }, this.periodicSyncInterval) + } + + periodicSyncPromise: Promise | undefined + async performPeriodicSync (): Promise { + try { + for (const inst of this.integrations.values()) { + await this.repositoryManager.reloadRepositories(inst) + } + this.triggerUpdate() + } catch (err: any) { + Analytics.handleError(err) + } + + this.periodicSyncPromise = undefined + } + + private async findPerson (userInfo: UserInfo, userName: string): Promise> { + let person: Ref | undefined + // try to find by account. + if (userInfo.email != null) { + const personAccount = await this.client.findOne(contact.class.PersonAccount, { email: userInfo.email }) + person = personAccount?.person + } + + if (person === undefined) { + const channel = await this.client.findOne(contact.class.Channel, { + provider: contact.channelProvider.GitHub, + value: userInfo.login + }) + person = channel?.attachedTo as Ref + } + + if (person === undefined) { + // We need to create some person to identify this account. + person = await this._client.createDoc(contact.class.Person, contact.space.Contacts, { + name: userName, + avatarType: AvatarType.EXTERNAL, + avatarProps: { url: userInfo.avatarUrl }, + city: '', + comments: 0, + channels: 0, + attachments: 0 + }) + await this._client.addCollection( + contact.class.Channel, + contact.space.Contacts, + person, + contact.class.Person, + 'channels', + { + provider: contact.channelProvider.GitHub, + value: userInfo.login + } + ) + if (userInfo.email != null && userInfo.email.trim() !== '') { + await this._client.addCollection( + contact.class.Channel, + contact.space.Contacts, + person, + contact.class.Person, + 'channels', + { + provider: contact.channelProvider.Email, + value: userInfo.email + } + ) + } + } + return person + } + + async getGithubLogin (container: IntegrationContainer, person: Ref): Promise { + const accounts = await this.liveQuery.queryFind(contact.class.PersonAccount, {}) + const acc = accounts.find((it) => it.person === person && it.email.startsWith('github:')) + if (acc === undefined) { + return // Nobody, will use system account. + } + const login = acc.email.substring(7) + let info = await this.liveQuery.findOne(github.class.GithubUserInfo, { login }) + if (info === undefined) { + // We need to retrieve info for login + const response: any = await container.octokit?.graphql( + `query($login: String!) { + user(login: $login) { + id + email + login + name + avatarUrl + } + }`, + { + login + } + ) + + info = response.user + await this._client.createDoc(github.class.GithubUserInfo, contact.space.Contacts, info as Data) + } + return info + } + + async syncUserData (ctx: MeasureContext, users: GithubUserRecord[]): Promise { + // Let's sync information about users and send some details + for (const record of users) { + if (record.error !== undefined) { + // Skip accounts with error + continue + } + const account = await this._client.findOne(contact.class.PersonAccount, { email: `github:${record._id}` }) + const userAuth = await this._client.findOne(github.class.GithubAuthentication, { login: record._id }) + const person = await this._client.findOne(contact.class.Person, { _id: account?.person }) + if (account === undefined || userAuth === undefined || person === undefined) { + continue + } + const accountRef = record.accounts[this.workspace.name] + try { + await this.platform.checkRefreshToken(record, true) + + const ops = new TxOperations(this.client, accountRef) + await syncUser(ctx, record, userAuth, ops, accountRef) + } catch (err: any) { + await this.platform.revokeUserAuth(record) + if (err.response?.data?.message !== 'Bad credentials') { + ctx.error(`Failed to sync user ${record._id}`, err) + Analytics.handleError(err) + } + if (userAuth !== undefined) { + await this._client.update( + userAuth, + { + error: errorToObj(err) + }, + undefined, + Date.now(), + accountRef + ) + } + } + } + } + + async getOctokit (account: Ref): Promise { + let record = await this.platform.getAccountByRef(this.workspace.name, account) + + // const accountRef = this.accounts.find((it) => it._id === account) + const accountRef = await this._client.findOne(contact.class.PersonAccount, { _id: account }) + if (record === undefined) { + if (accountRef !== undefined) { + const accounts = await this._client.findAll(contact.class.PersonAccount, {}) + const allAccounts = accounts.filter((it) => it.person === accountRef.person) + for (const aa of allAccounts) { + record = await this.platform.getAccountByRef(this.workspace.name, aa._id) + if (record !== undefined) { + break + } + } + } + } + // Check and refresh token if required. + if (record !== undefined) { + this.ctx.info('get octokit', { account, recordId: record._id }) + await this.platform.checkRefreshToken(record) + return new Octokit({ + auth: record.token, + client_id: config.ClientID, + client_secret: config.ClientSecret + }) + } + + // We need to inform user, he need to authorize this account with github. + if (accountRef !== undefined) { + const person = await this.client.findOne(contact.class.Person, { _id: accountRef.person }) + if (person !== undefined) { + await createNotification(this._client, person, { + user: account, + message: github.string.AuthenticatedWithGithubRequired, + props: {} + }) + } + } + this.ctx.info('get octokit: return bot', { account }) + } + + async isPlatformUser (account: Ref): Promise { + let record = await this.platform.getAccountByRef(this.workspace.name, account) + const accountRef = await this._client.findOne(contact.class.PersonAccount, { _id: account }) + if (record === undefined) { + if (accountRef !== undefined) { + const accounts = await this._client.findAll(contact.class.PersonAccount, {}) + const allAccounts = accounts.filter((it) => it.person === accountRef.person) + for (const aa of allAccounts) { + record = await this.platform.getAccountByRef(this.workspace.name, aa._id) + if (record !== undefined) { + break + } + } + } + } + // Check and refresh token if required. + return record !== undefined && accountRef !== undefined + } + + async uploadFile (patch: string, file?: string): Promise { + const id: string = file ?? generateId() + await this.storageAdapter.put(this.ctx, this.workspace, id, patch, 'text/x-patch', patch.length) + return await this.storageAdapter.stat(this.ctx, this.workspace, id) + } + + integrationRepositories: WithLookup[] = [] + integrationsRaw: GithubIntegration[] = [] + + async getProjectType (type: Ref): Promise { + return await this._client.findOne(task.class.ProjectType, { _id: type }) + } + + async getTaskType (type: Ref): Promise { + return await this._client.findOne(task.class.TaskType, { _id: type }) + } + + async getTaskTypeOf (project: Ref, ofClass: Ref>): Promise { + const pType = await this.getProjectType(project) + for (const tsk of pType?.tasks ?? []) { + const task = await this.getTaskType(tsk) + if (task?.ofClass === ofClass) { + return task + } + } + } + + async getProjectStatuses (type: Ref | undefined): Promise { + if (type === undefined) return [] + + const statuses = await this.liveQuery.queryFind(core.class.Status, {}) + + const projectType = await this.getProjectType(type) + + const allowedTypes = new Set(projectType?.statuses.map((it) => it._id) ?? []) + return statuses.filter((it) => allowedTypes.has(it._id)) + } + + async getStatuses (type: Ref | undefined): Promise { + if (type === undefined) return [] + const taskType = await this.getTaskType(type) + + const statuses = await this.liveQuery.queryFind(core.class.Status, {}) + + const allowedTypes = new Set(taskType?.statuses ?? []) + return statuses.filter((it) => allowedTypes.has(it._id)) + } + + async init (): Promise { + this.registerNotifyHandler() + + await this.queryAccounts() + + await this.queryProjects() + + for (const { mapper } of this.mappers) { + await mapper.init(this) + } + + await new Promise((resolve) => { + this.liveQuery.query(github.class.GithubIntegration, {}, (res) => { + try { + if (equalExceptKeys(this.integrationsRaw, res, ['modifiedBy', 'modifiedOn'])) { + return + } + this.integrationsRaw = res + } finally { + resolve() + } + + this.triggerUpdate() + }) + }) + await new Promise((resolve) => { + this.liveQuery.query(github.class.GithubIntegrationRepository, {}, (res) => { + try { + if (equalExceptKeys(this.integrationRepositories, res, ['modifiedOn', 'modifiedBy'])) { + return + } + // We have some repository changed. + const existing = toIdMap(res) + const removed = this.integrationRepositories.filter((it) => !existing.has(it._id)) + this.integrationRepositories = res + + const clearCacheForRepo = (r: WithLookup): void => { + const i = Array.from(this.integrations.values()).find((it) => it.integration._id === r.attachedTo) + if (i !== undefined) { + for (const m of this.mappers) { + m.mapper.repositoryDisabled(i, r) + } + } + } + // In case enabled is change to false, we need to clean cache for repository. + for (const r of res) { + if (!r.enabled) { + clearCacheForRepo(r) + } + } + for (const o of removed) { + clearCacheForRepo(o) + } + } finally { + resolve() + } + + this.triggerUpdate() + }) + }) + + const userRecords = await this.platform.getUsers(this.workspace.name) + + await this.syncUserData(this.ctx, userRecords) + + this.triggerRequests = 1 + this.updateRequests = 1 + this.syncPromise = this.syncAndWait() + return true + } + + projects: GithubProject[] = [] + milestones: GithubMilestone[] = [] + + async queryProjects (): Promise { + await new Promise((resolve) => { + this.liveQuery.query(github.mixin.GithubProject, {}, (res) => { + let needRefresh = false + if (!equalExceptKeys(this.projects, res, ['sequence', 'modifiedOn', 'modifiedBy'])) { + needRefresh = true + } + this.projects = res + resolve() + if (needRefresh || this.projects.length !== res.length) { + // Do not trigger update if only sequence is changed. + this.triggerUpdate() + } + }) + }) + + await new Promise((resolve) => { + this.liveQuery.query(github.mixin.GithubMilestone, {}, (res) => { + let needRefresh = false + if (!equalExceptKeys(this.milestones, res, ['modifiedOn', 'modifiedBy'])) { + needRefresh = true + } + this.milestones = res + resolve() + if (needRefresh || this.milestones.length !== res.length) { + // Do not trigger update if only sequence is changed. + this.triggerUpdate() + } + }) + }) + } + + async updateIntegrations (): Promise { + await this.checkMapping() + for (const it of this.integrationsRaw) { + let current = this.integrations.get(it.installationId) + if (current === undefined) { + try { + const inst = this.installations.get(it.installationId) + if (inst === undefined) { + // will update on next update. + continue + } + current = { + integration: it, + octokit: inst.octokit, + installationId: it.installationId, + login: inst.login ?? '', + loginNodeId: inst.loginNodeId ?? '', + type: inst.type ?? 'User', + installationName: inst?.installationName ?? '', + enabled: true, + synchronized: new Set(), + projectStructure: new Map(), + syncLock: new Map() + } + this.integrations.set(it.installationId, current) + await this.repositoryManager.reloadRepositories(current) + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error('Error', { err }) + } + } else { + const inst = this.installations.get(it.installationId) + if (inst === undefined) { + // will update on next update. + continue + } + current.integration = it + await this.repositoryManager.reloadRepositories(current) + } + } + } + + private registerNotifyHandler (): void { + this.client.notify = (...tx: Tx[]) => { + void this.liveQuery + .tx(...tx) + .then(() => { + // Handle tx + const h = this._client.getHierarchy() + for (const t of tx) { + if (h.isDerived(t._class, core.class.TxCUD)) { + const cud = t as TxCUD + if (cud.objectClass === github.class.DocSyncInfo) { + this.triggerSync() + break + } + } + if (h.isDerived(t._class, core.class.TxApplyIf)) { + const applyop = t as TxApplyIf + for (const tt of applyop.txes) { + if (tt.objectClass === github.class.DocSyncInfo) { + this.triggerSync() + break + } + } + } + if (h.isDerived(t._class, core.class.TxWorkspaceEvent)) { + const evt = t as TxWorkspaceEvent + if (evt.event === WorkspaceEvent.BulkUpdate) { + // Just trigger for any bulk update + this.triggerUpdate() + break + } + } + } + }) + .catch((err) => { + this.ctx.error('error during live query', { err }) + Analytics.handleError(err) + }) + } + } + + private async queryAccounts (): Promise { + const updateAccounts = async (accounts: PersonAccount[]): Promise => { + const persons = await this.liveQuery.queryFind(contact.class.Person, { + _id: { $in: accounts.map((it) => it.person) } + }) + const h = this.client.getHierarchy() + for (const a of accounts) { + if (a.email.startsWith('github:')) { + const login = a.email.substring(7) + const person = persons.find((it) => it._id === a.person) + if (person !== undefined) { + // #1 check if person has GithubUser mixin. + if (!h.hasMixin(person, github.mixin.GithubUser)) { + await this._client.createMixin(person._id, person._class, person.space, github.mixin.GithubUser, { + url: `https://github.com/${login}` + }) + } else { + const ghu = h.as(person, github.mixin.GithubUser) + if (ghu.url !== `https://github.com/${login}`) { + await this._client.updateMixin(person._id, person._class, person.space, github.mixin.GithubUser, { + url: `https://github.com/${login}` + }) + } + } + // #2 check if person has contact github and if not add it. + const channel = await this._client.findOne(contact.class.Channel, { + provider: contact.channelProvider.GitHub, + value: login, + attachedTo: person._id + }) + if (channel === undefined) { + await this._client.addCollection( + contact.class.Channel, + person.space, + person._id, + contact.class.Person, + 'channels', + { + provider: contact.channelProvider.GitHub, + value: login + } + ) + } + } + } + } + } + await new Promise((resolve, reject) => { + this.liveQuery.query(contact.class.PersonAccount, {}, (res) => { + void updateAccounts(res).then(resolve).catch(reject) + }) + }) + } + + async performExternalSync ( + projects: GithubProject[], + repositories: GithubIntegrationRepository[], + field: ExternalSyncField, + version: string + ): Promise { + // Find all classes with e + const query: DocumentQuery = { + [field]: { $nin: [version, '#'] }, + space: { $in: Array.from(projects.map((it) => it._id)) }, + repository: { $in: Array.from(repositories.map((it) => it._id)) }, + url: { $ne: '' } + } + if (field === 'derivedVersion') { + const classes: Ref>[] = [] + for (const m of this.mappers) { + if (m.mapper.externalDerivedSync) { + classes.push(...m._class) + } + } + // We need check only mappers, with support to this sync kind + query.objectClass = { $in: classes } + } + + const docs = await this._client.findAll(github.class.DocSyncInfo, query, { + limit: 50, + sort: { modifiedOn: SortingOrder.Ascending } + }) + if (docs.length === 0) { + return false + } + // Group changes by class and retrieve source documents. + const byRepository = this.groupByRepository(docs) + + const ints = Array.from(this.integrations.values()) + const derivedClient = new TxOperations(this.client, core.account.System, true) + for (const [repository, docs] of byRepository.entries()) { + const integration = ints.find((it) => repositories.find((q) => q._id === repository)) + if (integration?.octokit === undefined) { + continue + } + + const repo = repositories.find((it) => it._id === repository) + if (repo === undefined) { + continue + } + const prj = projects.find((it) => repo.githubProject === it._id) + if (prj === undefined) { + continue + } + + this.ctx.info('External Syncing', { name: repo.name, prj: prj.name, field, version, docs: docs.length }) + + const byClass = this.groupByClass(docs) + for (const [_class, _docs] of byClass.entries()) { + const mapper = this.mappers.find((it) => it._class.includes(_class))?.mapper + try { + await mapper?.externalSync(integration, derivedClient, field, _docs, repo, prj) + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error('failed to perform external sync', err) + } + } + } + return true + } + + previousWait = 0 + private groupByRepository (docs: FindResult): Map, DocSyncInfo[]> { + const byRepository = new Map, DocSyncInfo[]>() + + for (const d of docs) { + byRepository.set(d.repository as Ref, [ + ...(byRepository.get(d.repository as Ref) ?? []), + d + ]) + } + return byRepository + } + + groupByClass (docs: DocSyncInfo[]): Map>, DocSyncInfo[]> { + const byClass = new Map>, DocSyncInfo[]>() + + for (const d of docs) { + byClass.set(d.objectClass, [...(byClass.get(d.objectClass) ?? []), d]) + } + return byClass + } + + async applyMigrations (): Promise { + const key = 'lowerCaseDuplicates' + // We need to apply migrations if required. + const migration = await this.client.findOne(core.class.MigrationState, { + plugin: githubId, + state: key + }) + + const derivedClient = new TxOperations(this.client, core.account.System, true) + + if (migration === undefined) { + let modifiedOn = 0 + const limit = 1000 + while (true) { + const docs = ( + await this.client.findAll( + github.class.DocSyncInfo, + { url: { $ne: '' }, modifiedOn }, + { projection: { url: 1, modifiedOn: 1 } } + ) + ).concat( + await this.client.findAll( + github.class.DocSyncInfo, + { url: { $ne: '' }, modifiedOn: { $gt: modifiedOn } }, + { limit, sort: { modifiedOn: SortingOrder.Ascending }, projection: { url: 1, modifiedOn: 1 } } + ) + ) + if (docs.length > 0) { + modifiedOn = docs[docs.length - 1].modifiedOn + } else { + // No more elements + break + } + const ops = derivedClient.apply(generateId()) + + const uris: string[] = [] + // Check if some uris need to be lowercased + for (const d of docs) { + if (d.url.startsWith('http://') || d.url.startsWith('https://')) { + const lw = d.url.toLowerCase() + uris.push(lw) // This is for duplicate processing + if (lw !== d.url) { + await ops.update(d, { url: lw }) + } + } + } + await ops.commit() + // Try find delete duplicate info + const duplicates = groupByArray( + await this.client.findAll(github.class.DocSyncInfo, { url: { $in: uris } }), + (d) => d.url + ) + for (const [, v] of duplicates.entries()) { + // We need to delete a value without a document or any + if (v.length > 1) { + for (const d of v.slice(1)) { + try { + await derivedClient.remove(d) + await derivedClient.removeDoc(d.objectClass, d.space, d._id) + } catch (err: any) { + this.ctx.error('failed to clean duplicate item', { err }) + } + } + } + } + if (docs.length < limit) { + // We processed all items + break + } + } + + await derivedClient.createDoc(core.class.MigrationState, core.space.Configuration, { + plugin: githubId, + state: key + }) + } + } + + async syncAndWait (): Promise { + this.updateRequests = 1 + + await this.applyMigrations() + while (!this.closing) { + if (this.updateRequests > 0) { + this.updateRequests = 0 // Just in case + await this.updateIntegrations() + await this.performFullSync() + } + + const { projects, repositories } = await this.collectActiveProjects() + if (projects.length === 0 && repositories.length === 0) { + await this.waitChanges() + continue + } + + // Check if we have documents with external sync request's pending. + const hadExternalChanges = await this.performExternalSync( + projects, + repositories, + 'externalVersion', + githubExternalSyncVersion + ) + const hadSyncChanges = await this.performSync(projects, repositories) + + // Perform derived operations + // Sync derived external data, like pull request reviews, files etc. + const hadDerivedChanges = await this.performExternalSync( + projects, + repositories, + 'derivedVersion', + githubDerivedSyncVersion + ) + + if (!hadExternalChanges && !hadSyncChanges && !hadDerivedChanges) { + if (this.previousWait !== 0) { + this.ctx.info('Wait for changes:', { previousWait: this.previousWait }) + this.previousWait = 0 + } + // Wait until some sync documents will be modified, updated. + await this.waitChanges() + } + } + } + + private async performSync (projects: GithubProject[], repositories: GithubIntegrationRepository[]): Promise { + const _projects = projects.map((it) => it._id) + const _repositories = repositories.map((it) => it._id) + + const h = this.client.getHierarchy() + const sortCases = this.mappers + .map((it) => it._class) + .flat() + .map((it) => h.getDescendants(it)) + .flat() + .map((it, idx) => ({ + query: it, + index: idx + })) + + const docs = await this.ctx.with( + 'find-doc-sync-info', + {}, + async (ctx) => + await this._client.findAll( + github.class.DocSyncInfo, + { + needSync: { $ne: githubSyncVersion }, + externalVersion: { $in: [githubExternalSyncVersion, '#'] }, + space: { $in: _projects }, + repository: { $in: [null, ..._repositories] } + }, + { + limit: 50, + sort: { + objectClass: { + order: SortingOrder.Ascending, + cases: sortCases + } + } + } + ), + { _projects, _repositories } + ) + + // + + if (docs.length > 0) { + this.previousWait += docs.length + this.ctx.info('Syncing', { docs: docs.length }) + + await this.doSyncFor(docs) + } + return docs.length !== 0 + } + + async doSyncFor (docs: DocSyncInfo[]): Promise { + const byClass = this.groupByClass(docs) + + // We need to reorder based on our sync mappers + + for (const [_class, clDocs] of byClass.entries()) { + await this.syncClass(_class, clDocs) + } + } + + private async collectActiveProjects (): Promise<{ + projects: GithubProject[] + repositories: GithubIntegrationRepository[] + }> { + const projects: GithubProject[] = [] + const repositories: GithubIntegrationRepository[] = [] + + const allProjects = await this.liveQuery.queryFind(github.mixin.GithubProject, {}) + const allRepositories = await this.liveQuery.queryFind(github.class.GithubIntegrationRepository, { enabled: true }) + + for (const it of Array.from(this.integrations.values())) { + if (it.enabled) { + const _projects = allProjects.filter((p) => !syncConfig.MainProject || it.projectStructure.has(p._id)) + + const prjIds = new Set(_projects.map((it) => it._id)) + + const _repos = allRepositories.filter((r) => r.githubProject != null && prjIds.has(r.githubProject)) + repositories.push(..._repos) + + const repoProjects = new Set(_repos.map((it) => it.githubProject)) + + projects.push(..._projects.filter((p) => repoProjects.has(p._id))) + } + } + + if (process.env.GITHUB_DEBUG_SYNC === 'true' && projects.length === 0 && repositories.length === 0) { + return { projects: allProjects, repositories: allRepositories } + } + + return { projects, repositories } + } + + async checkMapping (): Promise { + for (const intgr of this.platform.integrations.filter((it) => it.workspace === this.workspace.name)) { + const integration = await this._client.findOne(github.class.GithubIntegration, { + installationId: intgr.installationId + }) + if (integration === undefined && this.installations.has(intgr.installationId)) { + const installation = this.installations.get(intgr.installationId) as InstallationRecord + await this._client.createDoc( + github.class.GithubIntegration, + core.space.Configuration, + { + alive: true, + installationId: intgr.installationId, + clientId: config.ClientID, + name: installation.installationName, + nodeId: installation.loginNodeId, + repositories: 0 + }, + generateId(), + Date.now(), + intgr.accountId + ) + this.triggerUpdate() + } else if (integration !== undefined) { + await this._client.diffUpdate(integration, { + alive: true + }) + } + } + } + + async syncClass (_class: Ref>, syncInfo: DocSyncInfo[]): Promise { + const externalDocs = await this._client.findAll(_class, { + _id: { $in: syncInfo.map((it) => it._id as Ref) } + }) + + const parents = await this._client.findAll(github.class.DocSyncInfo, { + url: { $in: syncInfo.map((it) => it.parent?.toLowerCase()).filter((it) => it) as string[] } + }) + + // Attached parents, for new documents. + const attachedTo = externalDocs + .filter((it) => this.client.getHierarchy().isDerived(it._class, core.class.AttachedDoc)) + .map((it) => (it as AttachedDoc).attachedTo as Ref) + .filter((it, idx, arr) => arr.indexOf(it) === idx) + + const attachedParents = await this._client.findAll(github.class.DocSyncInfo, { + _id: { $in: attachedTo } + }) + + const derivedClient = new TxOperations(this.client, core.account.System, true) + + const docsMap = new Map, Doc>(externalDocs.map((it) => [it._id as Ref, it])) + const orderedSyncInfo = [...syncInfo] + orderedSyncInfo.sort((a, b) => { + const adoc = docsMap.get(a._id as Ref) + const bdoc = docsMap.get(a._id as Ref) + return (bdoc?.createdOn ?? 0) - (adoc?.createdOn ?? 0) + }) + + for (const info of orderedSyncInfo) { + try { + const existing = externalDocs.find((it) => it._id === info._id) + const mapper = this.mappers.find((it) => it._class.includes(info.objectClass))?.mapper + if (mapper === undefined) { + this.ctx.info('No mapper for class', { objectClass: info.objectClass }) + await derivedClient.update(info, { + needSync: githubSyncVersion + }) + continue + } + const container = await this.getContainer(info.space) + const repo = container?.repository.find((it) => it._id === info.repository) + if (repo !== undefined && !repo.enabled) { + continue + } + + let parent = + info.parent !== undefined + ? parents.find((it) => it.url.toLowerCase() === info.parent?.toLowerCase()) + : undefined + if ( + parent === undefined && + existing !== undefined && + this.client.getHierarchy().isDerived(existing._class, core.class.AttachedDoc) + ) { + // Find with attached parent + parent = attachedParents.find((it) => it._id === (existing as AttachedDoc).attachedTo) + } + + if (existing !== undefined && existing.space !== info.space) { + // document is moved to non github project, so for github it like delete. + const targetProject = await this.client.findOne(github.mixin.GithubProject, { + _id: existing.space as Ref + }) + if (await mapper.handleDelete(existing, info, derivedClient, false, parent)) { + const h = this._client.getHierarchy() + await derivedClient.remove(info) + if (h.hasMixin(existing, github.mixin.GithubIssue)) { + const mixinData = this._client.getHierarchy().as(existing, github.mixin.GithubIssue) + await this._client.update( + mixinData, + { + url: '', + githubNumber: 0, + repository: '' as Ref + }, + false, + Date.now(), + existing.modifiedBy + ) + } + continue + } + if (targetProject !== undefined) { + // We need to sync into new project. + await derivedClient.update(info, { + external: null, + current: null, + url: '', + needSync: '', + externalVersion: '', + githubNumber: 0, + repository: null + }) + } + } + + if (info.deleted === true) { + if (await mapper.handleDelete(existing, info, derivedClient, true)) { + await derivedClient.remove(info) + continue + } + } + + const docUpdate = await this.ctx.withLog( + 'sync doc', + {}, + async (ctx) => await mapper.sync(existing, info, parent, derivedClient), + { url: info.url.toLowerCase() } + ) + if (docUpdate !== undefined) { + await derivedClient.update(info, docUpdate) + } + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error('failed to sync doc', { _id: info._id, objectClass: info.objectClass, error: err }) + // Mark to stop processing of document, before restart. + await derivedClient.update(info, { + error: errorToObj(err), + needSync: githubSyncVersion, + externalVersion: githubExternalSyncVersion + }) + } + } + } + + private async waitChanges (): Promise { + if (this.triggerRequests > 0 || this.updateRequests > 0) { + this.ctx.info('Trigger check pending:', { requests: this.triggerRequests, updates: this.updateRequests }) + this.triggerRequests = 0 + return + } + await new Promise((resolve) => { + let triggerTimeout: any + let updateTimeout: any + this.triggerSync = () => { + const was0 = this.triggerRequests === 0 + this.triggerRequests++ + if (triggerTimeout === undefined) { + triggerTimeout = setTimeout(() => { + triggerTimeout = undefined + if (was0) { + this.ctx.info('Sync triggered', { request: this.triggerRequests }) + } + resolve() + }, 50) // Small timeout to aggregate few bulk changes. + } + } + + this.triggerUpdate = () => { + const was0 = this.updateRequests === 0 + this.updateRequests++ + if (updateTimeout === undefined) { + updateTimeout = setTimeout(() => { + updateTimeout = undefined + if (was0) { + this.ctx.info('Sync update triggered', { requests: this.updateRequests }) + } + resolve() + }, 50) // Small timeout to aggregate few bulk changes. + } + } + // Wait up for every 60 seconds to refresh, just in case. + setTimeout(() => { + resolve() + }, 60 * 1000) + }) + } + + sync (): void { + this.triggerSync() + } + + async performFullSync (): Promise { + // Wait previous active sync + for (const integration of this.integrations.values()) { + await this.ctx.withLog('external sync', { installation: integration.installationName }, async () => { + if (!integration.enabled || integration.octokit === undefined) { + return + } + + const upd: DocumentUpdate = {} + if (integration.integration.byUser !== integration.login) { + upd.byUser = integration.login + } + if (integration.integration.type !== integration.type) { + upd.type = integration.type + } + if (integration.integration.clientId !== config.ClientID) { + upd.clientId = config.ClientID + } + + if (integration.integration.name !== integration.installationName || !integration.integration.alive) { + upd.name = integration.installationName + upd.alive = true + } + if (Object.keys(upd).length > 0) { + await this._client.diffUpdate(integration.integration, upd, Date.now(), integration.integration.createdBy) + this.triggerUpdate() + } + const derivedClient = new TxOperations(this.client, core.account.System, true) + + const { projects, repositories } = await this.collectActiveProjects() + + const _projects = projects.filter((it) => it.integration === integration.integration._id) + const _prjIds = new Set(_projects.map((it) => it._id)) + const _repositories = repositories.filter((it) => it.githubProject != null && _prjIds.has(it.githubProject)) + + // Cleanup broken synchronized documents + + while (true) { + const withError = await derivedClient.findAll( + github.class.DocSyncInfo, + { error: { $ne: null }, url: null }, + { limit: 50 } + ) + if (withError.length === 0) { + break + } + const ops = derivedClient.apply('cleanup_github' + generateId()) + for (const d of withError) { + await ops.remove(d) + } + await ops.commit() + } + + for (const { _class, mapper } of this.mappers) { + await this.ctx.withLog('external sync', { _class: _class.join(', ') }, async () => { + await mapper.externalFullSync(integration, derivedClient, _projects, _repositories) + }) + } + }) + } + } + + async handleEvent(requestClass: Ref>, integrationId: number | undefined, event: T): Promise { + if (integrationId === undefined) { + return + } + const integration = this.integrations.get(integrationId) + if (integration === undefined) { + return + } + const derivedClient = new TxOperations(this.client, core.account.System, true) + for (const { _class, mapper } of this.mappers) { + if (_class.includes(requestClass)) { + try { + await mapper.handleEvent(integration, derivedClient, event) + } catch (err: any) { + Analytics.handleError(err) + this.ctx.error('exception during processing of event:', { event, err }) + } + } + } + } + + async getProjectAndRepository ( + repositoryId: string + ): Promise<{ project?: GithubProject, repository?: GithubIntegrationRepository }> { + const repository = await this.liveQuery.findOne( + github.class.GithubIntegrationRepository, + { nodeId: repositoryId } + ) + if (repository?.githubProject == null) { + return {} + } + const project = await this.liveQuery.findOne(github.mixin.GithubProject, { _id: repository.githubProject }) + return { project, repository } + } + + static async create ( + platformWorker: PlatformWorker, + ctx: MeasureContext, + installations: Map, + workspace: WorkspaceIdWithUrl, + branding: Branding | null, + app: App, + storageAdapter: StorageAdapter, + reconnect: (workspaceId: string, event: ClientConnectEvent) => void + ): Promise { + ctx.info('Connecting to', { workspace: workspace.workspaceUrl, workspaceId: workspace.workspaceName }) + let client: Client | undefined + try { + client = await createPlatformClient(workspace.name, workspace.productId, 10000, (event: ClientConnectEvent) => { + reconnect(workspace.name, event) + }) + + const worker = new GithubWorker( + ctx, + platformWorker, + installations, + client, + app, + storageAdapter, + workspace, + branding + ) + ctx.info('Init worker', { workspace: workspace.workspaceUrl, workspaceId: workspace.workspaceName }) + if (await worker.init()) { + return worker + } + } catch (err: any) { + await client?.close() + } + } +} + +export async function syncUser ( + ctx: MeasureContext, + record: GithubUserRecord, + userAuth: WithLookup, + client: TxOperations, + account: Ref +): Promise { + const okit = new Octokit({ + auth: record.token, + client_id: config.ClientID, + client_secret: config.ClientSecret + }) + + const details = await fetchViewerDetails(okit) + + const dta = { + followers: details.viewer.followers.totalCount, + following: details.viewer.following.totalCount, + + openIssues: details.viewer.openIssues.totalCount, + closedIssues: details.viewer.closedIssues.totalCount, + openPRs: details.viewer.openPRs.totalCount, + mergedPRs: details.viewer.mergedPRs.totalCount, + closedPRs: details.viewer.closedPRs.totalCount, + + repositories: details.viewer.repositories.totalCount, + starredRepositories: details.viewer.starredRepositories.totalCount + } + + await client.diffUpdate( + userAuth, + { + email: details.viewer.email, + url: details.viewer.url, + name: details.viewer.name, + bio: details.viewer.bio, + location: details.viewer.location, + company: details.viewer.company, + avatar: details.viewer.avatarUrl, + updatedAt: new Date(details.viewer.updatedAt ?? ''), + repositoryDiscussions: details.viewer.repositoryDiscussions.totalCount, + organizations: details.viewer.organizations, + nodeId: details.viewer.id, + ...dta + }, + undefined, + account + ) +} diff --git a/services/github/pod-github/tsconfig.json b/services/github/pod-github/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/github/pod-github/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/github/readme.md b/services/github/readme.md new file mode 100644 index 0000000000..260f5922e6 --- /dev/null +++ b/services/github/readme.md @@ -0,0 +1,17 @@ +# Tasks for Github integration + +## Project management from Huly (Postponed) + +1. Observe all existing/closed projects and retrieve all custom field values and store them into Huly custom fields. It could be separate set of mixins possible. +2. On update of custom fields, propagate changes to appropriate project/field. +3. Status (Huly) is more rich in compare to issue/pull request (open/closed) status of Github. But for Projects, it could be "single_select" option to be used. But github right now doesn't provide a way to manage existing fields. So we could not maintain state of status field on github project. We only create/delete it. + Possible workarounds: + + - Map Huly Status to Project Status field, and ask user to perform manual definitions of required status values. We will map by status name if appropriate value will be found. + - On changes in Platform Status, we should show appropriate message about requirements to manage status on Github project. + - On changes on Github side we could add necessary status values into Huly synchronized projects. + +4. A list of issue for ProjectsV2 graphql APIs missing on GitHub side. + https://github.com/orgs/community/discussions/61457 +5. Enum values could be mapped to Github single_select values, but they could be created only from Github platform to prevent API limitations. +6. Text/number fields could be created from both sides. diff --git a/services/github/server-github-model/.eslintrc.js b/services/github/server-github-model/.eslintrc.js new file mode 100644 index 0000000000..c1cf82cba0 --- /dev/null +++ b/services/github/server-github-model/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/github/server-github-model/.npmignore b/services/github/server-github-model/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/github/server-github-model/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/github/server-github-model/config/rig.json b/services/github/server-github-model/config/rig.json new file mode 100644 index 0000000000..2f6be36605 --- /dev/null +++ b/services/github/server-github-model/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "model" +} diff --git a/services/github/server-github-model/package.json b/services/github/server-github-model/package.json new file mode 100644 index 0000000000..6d1078cc19 --- /dev/null +++ b/services/github/server-github-model/package.json @@ -0,0 +1,41 @@ +{ + "name": "@hcengineering/server-github-model", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Hardcore Engineering Inc.", + "template": "@hcengineering/model-package", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/core": "^0.6.32", + "@hcengineering/model": "^0.6.11", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/server-github": "^0.6.0", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/github": "^0.6.0", + "@hcengineering/notification": "^0.6.23", + "@hcengineering/server-notification": "^0.6.1", + "@hcengineering/time": "^0.6.0" + } +} diff --git a/services/github/server-github-model/src/index.ts b/services/github/server-github-model/src/index.ts new file mode 100644 index 0000000000..c1d6c06dbe --- /dev/null +++ b/services/github/server-github-model/src/index.ts @@ -0,0 +1,24 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +import { type Builder } from '@hcengineering/model' + +import core from '@hcengineering/core' +import serverCore from '@hcengineering/server-core' +import serverGithub from '@hcengineering/server-github' +import time from '@hcengineering/time' + +export { serverGithubId } from '@hcengineering/server-github' + +export function createModel (builder: Builder): void { + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverGithub.trigger.OnProjectChanges, + isAsync: true + }) + + // We should skip activity github mixin stuff. + builder.createDoc(time.class.TodoAutomationHelper, core.space.Model, { + onDoneTester: serverGithub.functions.TodoDoneTester + }) +} diff --git a/services/github/server-github-model/tsconfig.json b/services/github/server-github-model/tsconfig.json new file mode 100644 index 0000000000..9765b6086d --- /dev/null +++ b/services/github/server-github-model/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/github/server-github-resources/.eslintrc.js b/services/github/server-github-resources/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/github/server-github-resources/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/github/server-github-resources/.npmignore b/services/github/server-github-resources/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/github/server-github-resources/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/github/server-github-resources/config/rig.json b/services/github/server-github-resources/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/github/server-github-resources/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/github/server-github-resources/jest.config.js b/services/github/server-github-resources/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/github/server-github-resources/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/github/server-github-resources/package.json b/services/github/server-github-resources/package.json new file mode 100644 index 0000000000..204f20d2aa --- /dev/null +++ b/services/github/server-github-resources/package.json @@ -0,0 +1,49 @@ +{ + "name": "@hcengineering/server-github-resources", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5" + }, + "dependencies": { + "@hcengineering/chunter": "^0.6.20", + "@hcengineering/core": "^0.6.32", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/github": "^0.6.0", + "@hcengineering/tracker": "^0.6.24", + "@hcengineering/time": "^0.6.0", + "@hcengineering/server-github": "^0.6.0" + } +} diff --git a/services/github/server-github-resources/src/index.ts b/services/github/server-github-resources/src/index.ts new file mode 100644 index 0000000000..95de68baf3 --- /dev/null +++ b/services/github/server-github-resources/src/index.ts @@ -0,0 +1,242 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// + +import chunter from '@hcengineering/chunter' +import contact, { PersonAccount } from '@hcengineering/contact' +import core, { + AttachedDoc, + Doc, + DocumentUpdate, + Hierarchy, + Ref, + Space, + Storage, + Tx, + TxCUD, + TxCollectionCUD, + TxProcessor, + TxUpdateDoc, + systemAccountEmail, + type Class, + type TxMixin +} from '@hcengineering/core' +import { TriggerControl } from '@hcengineering/server-core' +import time, { ToDo } from '@hcengineering/time' +import tracker from '@hcengineering/tracker' +import github, { DocSyncInfo, GithubProject } from '@hcengineering/github' + +/** + * @public + */ +export async function OnProjectChanges (tx: Tx, control: TriggerControl): Promise { + const ltx = TxProcessor.extractTx(tx) + + if (ltx._class === core.class.TxMixin && (ltx as TxMixin).mixin === github.mixin.GithubIssue) { + const mix = ltx as TxMixin + // Do not spend time to wait for trigger processing + await updateDocSyncInfo(control, tx, mix.objectSpace, mix) + return [] + } + + if (control.hierarchy.isDerived(ltx._class, core.class.TxCUD)) { + const cud = ltx as TxCUD + + let space: Ref = cud.objectSpace + + if (cud._class === core.class.TxUpdateDoc) { + const upd = cud as TxUpdateDoc + if (upd.operations.space != null) { + space = upd.operations.space + } + } + + if (isDocSyncUpdateRequired(control.hierarchy, cud)) { + // Do not spend time to wait for trigger processing + await updateDocSyncInfo(control, tx, space, cud) + } + if (control.hierarchy.isDerived(cud.objectClass, time.class.ToDo)) { + if (tx._class === core.class.TxCollectionCUD) { + const coll = tx as TxCollectionCUD + if (control.hierarchy.isDerived(coll.objectClass, github.class.GithubPullRequest)) { + // Ok we got todo change for pull request, let's mark it for sync. + return [ + control.txFactory.createTxUpdateDoc( + github.class.DocSyncInfo, + coll.objectSpace, + coll.objectId as Ref, + { + needSync: '' + } + ) + ] + } + } + } + } + return [] +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export default async () => ({ + trigger: { + OnProjectChanges + }, + functions: { + TodoDoneTester + } +}) + +async function TodoDoneTester ( + client: { + findAll: Storage['findAll'] + hierarchy: Hierarchy + }, + todo: ToDo +): Promise { + if (client.hierarchy.hasMixin(todo, github.mixin.GithubTodo)) { + return false + } + return true +} + +async function updateDocSyncInfo ( + control: TriggerControl, + tx: Tx, + space: Ref, + cud: { + _class: Ref> + objectId: Ref + objectClass: Ref> + } +): Promise { + const checkTx = (tx: Tx): boolean => + control.hierarchy.isDerived(tx._class, core.class.TxCUD) && + (tx as TxCUD).objectClass === github.class.DocSyncInfo && + (tx as TxCUD).objectId === cud.objectId + + const txes = [...control.txes.apply, ...control.txes.result, ...control.operationContext.derived.txes] + // Check already captured Txes + for (const i of txes) { + if (checkTx(i)) { + // We have sync doc create request already. + return + } + } + + const [account] = await control.modelDb.findAll(contact.class.PersonAccount, { + _id: tx.modifiedBy as Ref + }) + // Do not modify state if is modified by github service. + if (account === undefined) { + return + } + + const projects = await control.queryFind(github.mixin.GithubProject, {}, { projection: { _id: 1 } }) + if (projects.some((it) => it._id === (space as Ref))) { + const [sdoc] = await control.findAll(github.class.DocSyncInfo, { _id: cud.objectId as Ref }) + // We need to check if sync doc is already exists. + if (sdoc === undefined) { + // Created by non github integration + // We need to create the doc sync info + await createSyncDoc(control, cud, tx, space) + } else { + // We need to create the doc sync info + await updateSyncDoc(control, cud, space, sdoc) + } + } +} + +function isDocSyncUpdateRequired (h: Hierarchy, coll: TxCUD): boolean { + return ( + h.isDerived(coll.objectClass, tracker.class.Issue) || + h.isDerived(coll.objectClass, chunter.class.ChatMessage) || + h.isDerived(coll.objectClass, github.class.GithubReviewComment) || + h.isDerived(coll.objectClass, github.class.GithubReview) || + h.isDerived(coll.objectClass, github.class.GithubReviewThread) || + h.isDerived(coll.objectClass, tracker.class.Milestone) + ) +} + +async function updateSyncDoc ( + control: TriggerControl, + cud: { + _class: Ref> + objectId: Ref + objectClass: Ref> + }, + space: Ref, + info: DocSyncInfo +): Promise { + const data: DocumentUpdate = + cud._class === core.class.TxRemoveDoc + ? { + needSync: '', + deleted: true + } + : { + needSync: '' + } + if (info.space !== space) { + data.externalVersion = '#' // We need to put this one to handle new documents.) + data.space = space + } + await control.apply([ + control.txFactory.createTxUpdateDoc( + github.class.DocSyncInfo, + info.space, + cud.objectId as Ref, + data + ) + ]) + + control.operationContext.derived.targets.github = (it) => { + if (control.hierarchy.isDerived(it._class, core.class.TxCUD)) { + if ((it as TxCUD).objectClass === github.class.DocSyncInfo) { + return [systemAccountEmail] + } + } + } +} + +async function createSyncDoc ( + control: TriggerControl, + cud: { + _class: Ref> + objectId: Ref + objectClass: Ref> + }, + tx: Tx, + space: Ref +): Promise { + const data: DocumentUpdate = { + url: '', + githubNumber: 0, + repository: null, + objectClass: cud.objectClass, + externalVersion: '#', // We need to put this one to handle new documents. + needSync: '', + derivedVersion: '' + } + if (tx._class === core.class.TxCollectionCUD) { + const coll = tx as TxCollectionCUD + // Collection CUD, we could assign attachedTo + data.attachedTo = coll.objectId + } + + await control.apply([ + control.txFactory.createTxCreateDoc( + github.class.DocSyncInfo, + space, + data, + cud.objectId as Ref + ) + ]) + control.operationContext.derived.targets.github = (it) => { + if (control.hierarchy.isDerived(it._class, core.class.TxCUD)) { + if ((it as TxCUD).objectClass === github.class.DocSyncInfo) { + return [systemAccountEmail] + } + } + } +} diff --git a/services/github/server-github-resources/tsconfig.json b/services/github/server-github-resources/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/github/server-github-resources/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/github/server-github/.eslintrc.js b/services/github/server-github/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/github/server-github/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/github/server-github/.npmignore b/services/github/server-github/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/github/server-github/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/github/server-github/config/rig.json b/services/github/server-github/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/github/server-github/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/github/server-github/jest.config.js b/services/github/server-github/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/github/server-github/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/github/server-github/package.json b/services/github/server-github/package.json new file mode 100644 index 0000000000..3878dfff64 --- /dev/null +++ b/services/github/server-github/package.json @@ -0,0 +1,47 @@ +{ + "name": "@hcengineering/server-github", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "test": "jest --passWithNoTests --silent", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@types/node": "~20.11.16", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5" + }, + "dependencies": { + "@hcengineering/core": "^0.6.32", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/server-notification": "^0.6.1", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/time": "^0.6.0", + "@hcengineering/github": "^0.6.0" + } +} diff --git a/services/github/server-github/src/index.ts b/services/github/server-github/src/index.ts new file mode 100644 index 0000000000..17c44599d6 --- /dev/null +++ b/services/github/server-github/src/index.ts @@ -0,0 +1,31 @@ +// +// Copyright © 2021, 2023 Hardcore Engineering Inc. +// +// + +import { Ref } from '@hcengineering/core' +import type { Metadata, Plugin, Resource } from '@hcengineering/platform' +import { plugin } from '@hcengineering/platform' +import { TriggerFunc } from '@hcengineering/server-core' +import { TodoDoneTester } from '@hcengineering/time' +import { GithubProject } from '@hcengineering/github' + +/** + * @public + */ +export const serverGithubId = 'server-github' as Plugin + +/** + * @public + */ +export default plugin(serverGithubId, { + trigger: { + OnProjectChanges: '' as Resource + }, + functions: { + TodoDoneTester: '' as Resource + }, + metadata: { + GithubProjects: '' as Metadata>> + } +}) diff --git a/services/github/server-github/tsconfig.json b/services/github/server-github/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/github/server-github/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/gmail/pod-gmail/.eslintrc.js b/services/gmail/pod-gmail/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/gmail/pod-gmail/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/gmail/pod-gmail/.npmignore b/services/gmail/pod-gmail/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/gmail/pod-gmail/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/gmail/pod-gmail/Dockerfile b/services/gmail/pod-gmail/Dockerfile new file mode 100644 index 0000000000..edc6643474 --- /dev/null +++ b/services/gmail/pod-gmail/Dockerfile @@ -0,0 +1,9 @@ + +FROM node:20-alpine + +WORKDIR /usr/src/app + +COPY bundle/bundle.js ./ + +EXPOSE 8087 +CMD [ "node", "bundle.js" ] diff --git a/services/gmail/pod-gmail/config/rig.json b/services/gmail/pod-gmail/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/gmail/pod-gmail/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/gmail/pod-gmail/jest.config.js b/services/gmail/pod-gmail/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/gmail/pod-gmail/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/gmail/pod-gmail/package.json b/services/gmail/pod-gmail/package.json new file mode 100644 index 0000000000..86eabecc2d --- /dev/null +++ b/services/gmail/pod-gmail/package.json @@ -0,0 +1,79 @@ +{ + "name": "@hcengineering/pod-gmail", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "_phase:bundle": "rushx bundle", + "_phase:docker-build": "rushx docker:build", + "_phase:docker-staging": "rushx docker:staging", + "bundle": "mkdir -p bundle && esbuild src/index.ts --bundle --platform=node > bundle/bundle.js", + "docker:build": "../../../common/scripts/docker_build.sh hardcoreeng/gmail", + "docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/gmail staging", + "docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/gmail", + "run-local": "cross-env ts-node src/index.ts", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@tsconfig/node16": "^1.0.4", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/node": "~20.11.16", + "@types/node-fetch": "~2.6.2", + "@types/ws": "^8.5.11", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "esbuild": "^0.20.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "prettier": "^3.1.0", + "ts-node-dev": "^2.0.0", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/attachment": "^0.6.14", + "@hcengineering/client": "^0.6.18", + "@hcengineering/client-resources": "^0.6.27", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/core": "^0.6.32", + "@hcengineering/gmail": "^0.6.22", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/setting": "^0.6.17", + "@hcengineering/server-client": "^0.6.0", + "cors": "^2.8.5", + "dotenv": "~16.0.0", + "express": "^4.19.2", + "fast-equals": "^5.0.1", + "file-api": "^0.10.4", + "form-data": "^4.0.0", + "googleapis": "^122.0.0", + "google-auth-library": "^8.0.2", + "gaxios": "^5.0.1", + "jwt-simple": "^0.5.6", + "mongodb": "^6.8.0", + "node-fetch": "^2.6.6", + "ws": "^8.18.0" + } +} diff --git a/services/gmail/pod-gmail/src/base64.ts b/services/gmail/pod-gmail/src/base64.ts new file mode 100644 index 0000000000..1ed9b64fbf --- /dev/null +++ b/services/gmail/pod-gmail/src/base64.ts @@ -0,0 +1,26 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export function encode64 (str: string): string { + return Buffer.from(str).toString('base64') +} + +export function decode64 (str: string): string { + return Buffer.from(str, 'base64').toString('utf8') +} + +export function arrayBufferToBase64 (buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString('base64') +} diff --git a/services/gmail/pod-gmail/src/client.ts b/services/gmail/pod-gmail/src/client.ts new file mode 100644 index 0000000000..3720e89cb2 --- /dev/null +++ b/services/gmail/pod-gmail/src/client.ts @@ -0,0 +1,23 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Client } from '@hcengineering/core' +import { createClient, getTransactorEndpoint } from '@hcengineering/server-client' + +export async function getClient (token: string): Promise { + const endpoint = await getTransactorEndpoint(token) + return await createClient(token, endpoint) +} diff --git a/services/gmail/pod-gmail/src/config.ts b/services/gmail/pod-gmail/src/config.ts new file mode 100644 index 0000000000..648b3c4c16 --- /dev/null +++ b/services/gmail/pod-gmail/src/config.ts @@ -0,0 +1,76 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +interface Config { + Port: number + + MongoURI: string + MongoDB: string + AccountsURL: string + UploadUrl: string + ServiceID: string + Secret: string + Credentials: string + WATCH_TOPIC_NAME: string + SystemEmail: string + FooterMessage: string +} + +const envMap: { [key in keyof Config]: string } = { + Port: 'PORT', + + MongoURI: 'MONGO_URI', + MongoDB: 'MONGO_DB', + + AccountsURL: 'ACCOUNTS_URL', + UploadUrl: 'UPLOAD_URL', + ServiceID: 'SERVICE_ID', + Secret: 'SECRET', + Credentials: 'Credentials', + SystemEmail: 'SYSTEM_EMAIL', + WATCH_TOPIC_NAME: 'WATCH_TOPIC_NAME', + FooterMessage: 'FOOTER_MESSAGE' +} + +const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined) + +const config: Config = (() => { + const params: Partial = { + Port: parseNumber(process.env[envMap.Port]) ?? 8087, + MongoDB: process.env[envMap.MongoDB] ?? 'gmail-service', + MongoURI: process.env[envMap.MongoURI], + AccountsURL: process.env[envMap.AccountsURL], + UploadUrl: process.env[envMap.UploadUrl], + ServiceID: process.env[envMap.ServiceID] ?? 'gmail-service', + Secret: process.env[envMap.Secret], + SystemEmail: process.env[envMap.SystemEmail] ?? 'anticrm@hc.engineering', + Credentials: process.env[envMap.Credentials], + WATCH_TOPIC_NAME: process.env[envMap.WATCH_TOPIC_NAME], + FooterMessage: process.env[envMap.FooterMessage] ?? '

Sent via Huly

' + } + + const missingEnv = (Object.keys(params) as Array) + .filter((key) => params[key] === undefined) + .map((key) => envMap[key]) + + if (missingEnv.length > 0) { + throw Error(`Missing env variables: ${missingEnv.join(', ')}`) + } + + return params as Config +})() + +export default config diff --git a/services/gmail/pod-gmail/src/error.ts b/services/gmail/pod-gmail/src/error.ts new file mode 100644 index 0000000000..9dabbff7ae --- /dev/null +++ b/services/gmail/pod-gmail/src/error.ts @@ -0,0 +1,24 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export class ApiError extends Error { + constructor ( + readonly code: string, + readonly message: string + ) { + super(message) + } +} diff --git a/services/gmail/pod-gmail/src/gmail.ts b/services/gmail/pod-gmail/src/gmail.ts new file mode 100644 index 0000000000..a6bb147d48 --- /dev/null +++ b/services/gmail/pod-gmail/src/gmail.ts @@ -0,0 +1,895 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import attachment, { Attachment } from '@hcengineering/attachment' +import core, { + AttachedData, + Client, + Data, + Doc, + Ref, + Space, + Timestamp, + TxCollectionCUD, + TxCreateDoc, + TxFactory, + TxOperations, + TxProcessor, + TxUpdateDoc, + Blob +} from '@hcengineering/core' +import gmail, { type Message, type NewMessage } from '@hcengineering/gmail' +import setting from '@hcengineering/setting' +import FormData from 'form-data' +import type { GaxiosResponse } from 'gaxios' +import type { Credentials, OAuth2Client } from 'google-auth-library' +import { gmail_v1, google } from 'googleapis' +import type { Collection, Db } from 'mongodb' +import fetch from 'node-fetch' +import { arrayBufferToBase64, decode64, encode64 } from './base64' +import config from './config' +import { GmailController } from './gmailController' +import { RateLimiter } from './rateLimiter' +import type { AttachedFile, Channel, History, ProjectCredentials, Token, User } from './types' +import { diffAttributes, isToken } from './utils' +import type { WorkspaceClient } from './workspaceClient' + +const SCOPES = ['https://www.googleapis.com/auth/gmail.modify'] + +const EMAIL_REGEX = + /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/ + +async function uploadFile ( + file: AttachedFile, + token: string, + opts?: { space: Ref, attachedTo: Ref } +): Promise { + const uploadUrl = config.UploadUrl + if (uploadUrl === undefined) { + throw Error('UploadURL is not defined') + } + + const data = new FormData() + const buffer = Buffer.from(file.file, 'base64') + data.append( + 'file', + Object.assign(buffer, { + lastModified: file.lastModified, + name: file.name, + type: file.type + }) + ) + + const params = + opts !== undefined + ? [ + ['space', opts.space], + ['attachedTo', opts.attachedTo] + ] + .filter((x): x is [string, Ref] => x[1] !== undefined) + .map(([name, value]) => `${name}=${value}`) + .join('&') + : '' + const suffix = params === '' ? params : `?${params}` + + const url = `${uploadUrl}${suffix}` + const resp = await fetch(url, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + token + }, + body: data + }) + + if (resp.status !== 200) { + throw Error(`Failed to upload file: ${resp.statusText}`) + } + + return (await resp.text()) as Ref +} + +function makeHTMLBody (message: NewMessage, from: string): string { + const str = [ + 'Content-Type: text/html; charset="UTF-8"\n', + 'MIME-Version: 1.0\n', + 'Content-Transfer-Encoding: 7bit\n', + `To: ${message.to} \n`, + `From: ${from} \n` + ] + + if (message.replyTo != null) { + str.push(`In-Reply-To: ${message.replyTo} \n`) + } + + if (message.copy != null && message.copy.length > 0) { + str.push(`Cc: ${message.copy.join(', ')} \n`) + } + + if (message.subject != null) { + str.push(`Subject: =?UTF-8?B?${encode64(message.subject)}?= \n`) + } + + str.push('\n\n') + str.push(addFooter(message.content)) + const res = str.join('') + return encode64(res).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') +} + +function addFooter (message: string): string { + if (config.FooterMessage === undefined || config.FooterMessage.trim() === '') return message + return message + config.FooterMessage.trim() +} + +function convertMessage ( + message: GaxiosResponse, + me: string +): AttachedData & { modifiedOn: Timestamp } { + const date = message.data.internalDate != null ? new Date(Number.parseInt(message.data.internalDate)) : new Date() + const from = getHeaderValue(message.data.payload, 'From') ?? '' + const to = getHeaderValue(message.data.payload, 'To') ?? '' + const copy = + getHeaderValue(message.data.payload, 'Cc') + ?.split(',') + .map((p) => p.trim()) ?? undefined + const incoming = !from.includes(me) + return { + modifiedOn: date.getTime(), + messageId: getHeaderValue(message.data.payload, 'Message-ID') ?? '', + replyTo: getHeaderValue(message.data.payload, 'In-Reply-To'), + copy, + content: getPartMessage(message.data.payload, 'text/html'), + textContent: getPartMessage(message.data.payload, 'text/plain').replace(/<[^>]*>/g, ''), + from, + to, + incoming, + subject: getHeaderValue(message.data.payload, 'Subject') ?? '', + sendOn: date.getTime() + } +} + +function getHeaderValue (payload: gmail_v1.Schema$MessagePart | undefined, name: string): string | undefined { + if (payload === undefined) return undefined + const headers = payload.headers + + return headers?.find((header) => header.name?.toLowerCase() === name.toLowerCase())?.value ?? undefined +} + +function getPartsMessage (parts: gmail_v1.Schema$MessagePart[] | undefined, mime: string): string { + let result = '' + if (parts !== undefined) { + const htmlPart = parts.find((part) => part.mimeType === mime) + const filtredParts = htmlPart !== undefined ? parts.filter((part) => part.mimeType === mime) : parts + for (const part of filtredParts ?? []) { + result += getPartMessage(part, mime) + } + } + return result +} + +function getPartMessage (part: gmail_v1.Schema$MessagePart | undefined, mime: string): string { + if (part === undefined) return '' + if (part.body?.data != null) { + return decode64(part.body.data) + } + return getPartsMessage(part.parts, mime) +} + +async function wait (sec: number): Promise { + await new Promise((resolve) => { + setTimeout(() => { + resolve() + }, sec * 1000) + }) +} + +export class GmailClient { + private readonly oAuth2Client: OAuth2Client + private readonly gmail: gmail_v1.Resource$Users + private me: string | undefined = undefined + private readonly tokens: Collection + private readonly histories: Collection + private readonly client: TxOperations + private watchTimer: NodeJS.Timeout | undefined = undefined + private refreshTimer: NodeJS.Timeout | undefined = undefined + private syncPromise: Promise | undefined = undefined + private readonly rateLimiter = new RateLimiter(1000, 200) // in fact 250, but let's make reserve + + private constructor ( + credentials: ProjectCredentials, + private readonly user: User, + mongo: Db, + client: Client, + private readonly workspace: WorkspaceClient + ) { + const { client_secret, client_id, redirect_uris } = credentials.web // eslint-disable-line + this.oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]) // eslint-disable-line + this.gmail = google.gmail({ version: 'v1', auth: this.oAuth2Client }).users + this.tokens = mongo.collection('tokens') + this.histories = mongo.collection('histories') + this.client = new TxOperations(client, this.user.userId) + } + + static async create ( + credentials: ProjectCredentials, + user: User | Token, + mongo: Db, + client: Client, + workspace: WorkspaceClient + ): Promise { + const gmailClient = new GmailClient(credentials, user, mongo, client, workspace) + if (isToken(user)) { + console.log('Setting token while creating', user.workspace, user.userId, user) + await gmailClient.setToken(user) + await gmailClient.refreshToken() + await gmailClient.addClient() + } + return gmailClient + } + + getAutUrl (redirectURL: string): string { + const { workspace, userId } = this.user + const authUrl = this.oAuth2Client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES, + state: encode64(JSON.stringify({ workspace, userId, redirectURL })) + }) + return authUrl + } + + async authorize (code: string): Promise { + const token = await this.oAuth2Client.getToken(code) + console.log('Setting token after authorize', this.user.workspace, this.user.userId, token) + await this.setToken(token.tokens) + await this.refreshToken() + await this.addClient() + void this.getNewMessagesAfterAuth() + + const me = await this.getMe() + const integrations = await this.client.findAll(setting.class.Integration, { + createdBy: this.user.userId, + type: gmail.integrationType.Gmail + }) + const updated = integrations.find((p) => p.disabled && p.value === me) + + for (const integration of integrations.filter((p) => p._id !== updated?._id)) { + await this.client.remove(integration) + } + if (updated !== undefined) { + await this.client.update(updated, { + disabled: false + }) + } else { + await this.client.createDoc(setting.class.Integration, core.space.Workspace, { + type: gmail.integrationType.Gmail, + disabled: false, + value: me + }) + } + } + + async getNewMessagesAfterAuth (): Promise { + const newMessages = await this.client.findAll(gmail.class.NewMessage, { + status: 'new', + from: this.user.userId + }) + await this.workspace.subscribeMessages() + for (const message of newMessages) { + await this.createMessage(message as NewMessage) + } + } + + async createMessage (message: NewMessage): Promise { + if (message.status === 'sent') return + try { + const me = await this.getMe() + const body = + message.attachments != null && message.attachments > 0 + ? await this.makeAttachmentsBody(message, me) + : makeHTMLBody(message, me) + await this.rateLimiter.take(100) + await this.gmail.messages.send({ + userId: 'me', + requestBody: { + raw: body + } + }) + + await this.client.updateDoc(message._class, message.space, message._id, { + status: 'sent' + }) + } catch (err: any) { + await this.client.updateDoc(message._class, message.space, message._id, { + status: 'error', + error: JSON.stringify(err) + }) + console.log('Create message error', this.user.workspace, this.user.userId, JSON.stringify(err)) + if (err?.response?.data?.error === 'invalid_grant') { + await this.refreshToken() + } + } + } + + private async checkErrorWithSignout (err: any): Promise { + if (this.checkError(err)) { + console.log('Invalid grant error', this.user.workspace, this.user.userId) + await this.workspace.signoutByUserId(this.user.userId, true) + return true + } + return false + } + + private checkError (err: any): boolean { + return err?.response?.data?.error === 'invalid_grant' + } + + async signout (byError: boolean = false): Promise { + try { + await this.close() + await this.oAuth2Client.revokeCredentials() + } catch {} + await this.tokens.deleteOne({ + userId: this.user.userId, + workspace: this.user.workspace + }) + const integration = await this.client.findOne(setting.class.Integration, { + createdBy: this.user.userId, + type: gmail.integrationType.Gmail + }) + if (integration !== undefined) { + if (byError) { + await this.client.update(integration, { disabled: true }) + } else { + await this.client.remove(integration) + } + } + } + + async sync (): Promise { + if (this.syncPromise !== undefined) { + await this.syncPromise + } + const history = await this.getHistory() + try { + if (history != null) { + this.syncPromise = this.partSync(history.historyId) + await this.syncPromise + } else { + this.syncPromise = this.fullSync() + await this.syncPromise + } + } finally { + this.syncPromise = undefined + } + } + + async startSync (): Promise { + await this.sync() + await this.watch() + // recall every 24 hours https://developers.google.com/gmail/api/guides/push + this.watchTimer = setInterval(() => { + void this.watch() + }, 86400000) + } + + async newChannel (value: string): Promise { + const query = `{from:${value} to:${value} cc:${value} bcc:${value}}` + await this.fullSync(query) + } + + private async getMe (): Promise { + if (this.me !== undefined) { + return this.me + } + for (let i = 0; i < 3; i++) { + await this.rateLimiter.take(1) + const profile = await this.gmail.getProfile({ + userId: 'me' + }) + this.me = profile.data.emailAddress ?? undefined + if (this.me !== undefined) return this.me + await wait(5) + } + throw new Error("Can't get profile email after 3 attempts") + } + + private async getCurrentToken (): Promise { + return await this.tokens.findOne({ + userId: this.user.userId, + workspace: this.user.workspace + }) + } + + private async updateCurrentToken (token: Credentials): Promise { + await this.tokens.updateOne( + { + userId: this.user.userId, + workspace: this.user.workspace + }, + { + $set: { + ...token + } + } + ) + } + + private async addClient (): Promise { + try { + const me = await this.getMe() + void this.startSync() + const controller = GmailController.getGmailController() + controller.addClient(me, this) + } catch (err) { + console.log('Add client error', this.user.workspace, this.user.userId, JSON.stringify(err)) + } + } + + private async setToken (token: Credentials): Promise { + try { + this.oAuth2Client.setCredentials(token) + await this.getMe() + } catch (err: any) { + console.log('Set token error', this.user.workspace, this.user.userId, JSON.stringify(err)) + if (this.checkError(err)) { + await this.signout(true) + } + throw err + } + } + + private async updateToken (token: Credentials): Promise { + try { + const currentToken = await this.getCurrentToken() + if (currentToken != null) { + await this.updateCurrentToken(token) + } else { + await this.tokens.insertOne({ + userId: this.user.userId, + workspace: this.user.workspace, + token: this.user.token, + ...token + }) + } + } catch (err) { + console.log('update token error', this.user.workspace, this.user.userId, JSON.stringify(err)) + } + } + + private async getHistory (): Promise { + return await this.histories.findOne({ + userId: this.user.userId, + workspace: this.user.workspace + }) + } + + private async clearHistory (): Promise { + await this.histories.deleteOne({ + userId: this.user.userId, + workspace: this.user.workspace + }) + } + + private async setHistoryId (historyId: string): Promise { + await this.histories.updateOne( + { + userId: this.user.userId, + workspace: this.user.workspace + }, + { + $set: { + historyId + } + }, + { upsert: true } + ) + } + + private async partSync (historyId: string): Promise { + let pageToken: string | undefined + let histories: GaxiosResponse + while (true) { + try { + await this.rateLimiter.take(2) + histories = await this.gmail.history.list({ + userId: 'me', + historyTypes: ['messageAdded'], + startHistoryId: historyId, + pageToken + }) + } catch (err) { + console.log('Part sync get history error', this.user.workspace, this.user.userId, err) + const signout = await this.checkErrorWithSignout(err) + if (signout) return + await this.clearHistory() + await this.sync() + return + } + const nextPageToken = histories.data.nextPageToken + const array = histories.data.history ?? [] + try { + for (const history of array) { + for (const message of history.messagesAdded ?? []) { + if (message.message?.id == null || (message.message.labelIds?.includes('DRAFT') ?? false)) { + continue + } + try { + const res = await this.getMessage(message.message.id) + await this.saveMessage(res) + } catch (err) { + console.log('Part sync message error', this.user.workspace, this.user.userId, message.message.id, err) + const signout = await this.checkErrorWithSignout(err) + if (signout) return + } + } + if (history.id != null) { + await this.setHistoryId(history.id) + } + } + if (nextPageToken == null) { + return + } else { + pageToken = nextPageToken + } + } catch (err) { + console.log('Part sync error', this.user.workspace, this.user.userId, err) + return + } + } + } + + private async fullSync (q?: string): Promise { + const pageToken: string | undefined = undefined + const query: gmail_v1.Params$Resource$Users$Messages$List = { + userId: 'me', + pageToken + } + if (q !== undefined) { + query.q = q + } + let currentHistoryId: string | undefined + console.log('start full sync', this.user.workspace, this.user.userId, q) + try { + const messagesIds: string[] = [] + while (true) { + await this.rateLimiter.take(5) + const messages = await this.gmail.messages.list(query) + if (query.pageToken == null) { + console.log('Total messages', this.user.workspace, this.user.userId, messages.data.resultSizeEstimate) + } + const ids = messages.data.messages?.map((p) => p.id) ?? [] + for (let index = 0; index < ids.length; index++) { + const id = ids[index] + if (id == null) continue + messagesIds.push(id) + } + if (messages.data.nextPageToken == null) { + console.log('Break, total new messages: ', messagesIds.length) + break + } + query.pageToken = messages.data.nextPageToken + } + for (let index = messagesIds.length - 1; index >= 0; index--) { + const id = messagesIds[index] + try { + const message = await this.getMessage(id) + const historyId = message.data.historyId + await this.saveMessage(message) + if (historyId != null && q === undefined) { + if (currentHistoryId == null || Number(currentHistoryId) < Number(historyId)) { + await this.setHistoryId(historyId) + currentHistoryId = historyId + } + } + } catch (err: any) { + console.log('Full sync message error', this.user.workspace, this.user.userId, err) + const signout = await this.checkErrorWithSignout(err) + if (signout) return + } + } + } catch (err) { + console.log('Full sync error', this.user.workspace, this.user.userId, err) + await this.checkErrorWithSignout(err) + } + } + + private async getMessage (id: string): Promise> { + await this.rateLimiter.take(5) + return await this.gmail.messages.get({ + id, + userId: 'me', + format: 'FULL' + }) + } + + private findChannels (res: AttachedData): Channel[] { + const recievers = res.copy != null ? [res.to, ...res.copy] : [res.to] + const values = res.incoming ? [res.from] : recievers + const result: Channel[] = [] + for (const value of values) { + const res = value.match(EMAIL_REGEX) + if (res !== null) { + const email = res[0] + const channel = this.workspace.getChannel(email) + if (channel !== undefined) result.push(channel) + } + } + return result + } + + private async saveMessage (message: GaxiosResponse): Promise { + const me = await this.getMe() + const res = convertMessage(message, me) + const channels = this.findChannels(res) + if (channels.length === 0) return + const attachments = await this.getPartFiles(message.data.payload, message.data.id ?? '') + const factory = new TxFactory(this.user.userId) + for (const channel of channels) { + const current = await this.client.findOne(gmail.class.Message, { + messageId: res.messageId, + attachedTo: channel._id + }) + const tx = current != null ? this.updateTx(res, current, factory, channel) : this.createTx(res, factory, channel) + if (tx !== undefined) { + const resultMessage = + current != null + ? TxProcessor.updateDoc2Doc(current, tx.tx as TxUpdateDoc) + : TxProcessor.createDoc2Doc(tx.tx as TxCreateDoc) + await this.client.tx(tx) + if (attachments.length > 0) { + const currentAttachemtns: Attachment[] = + current !== undefined + ? await this.client.findAll(attachment.class.Attachment, { attachedTo: current._id }) + : [] + for (const attachment of attachments) { + await this.addAttachement(attachment, resultMessage, currentAttachemtns) + } + } + } + } + } + + private async addAttachement (file: AttachedFile, message: Message, currentAttachemtns: Attachment[]): Promise { + try { + if (currentAttachemtns.findIndex((p) => p.name === file.name && p.lastModified === file.lastModified) !== -1) { + return + } + const uuid = await uploadFile(file, this.user.token, { space: message.space, attachedTo: message._id }) + const data: AttachedData = { + name: file.name, + file: uuid as any, + type: file.type ?? 'undefined', + size: file.size ?? Buffer.from(file.file, 'base64').length, + lastModified: file.lastModified + } + await this.client.addCollection( + attachment.class.Attachment, + message.space, + message._id, + message._class, + 'attachments', + data + ) + } catch (err: any) { + console.log('Add attachment error', this.user.workspace, this.user.userId, JSON.stringify(err)) + } + } + + private async getPartFiles ( + part: gmail_v1.Schema$MessagePart | undefined, + messageId: string + ): Promise { + if (part === undefined) return [] + if (part.filename != null && part.filename.length > 0) { + if (part.body?.attachmentId != null) { + const attachment = await this.getAttachmentById(part.body?.attachmentId, messageId) + if (attachment.data == null) return [] + return [ + { + file: attachment.data, + name: part.filename, + type: part.mimeType ?? undefined, + size: attachment.size ?? undefined, + lastModified: new Date().getTime() + } + ] + } + if (part.body?.data == null) return [] + return [ + { + file: part.body.data, + name: part.filename, + type: part.mimeType ?? undefined, + size: part.body.size ?? undefined, + lastModified: new Date().getTime() + } + ] + } + return await this.getPartsFiles(part.parts, messageId) + } + + private async getAttachmentById (attachmentId: string, messageId: string): Promise { + await this.rateLimiter.take(5) + const res = await this.gmail.messages.attachments.get({ id: attachmentId, userId: 'me', messageId }) + return res.data + } + + private async getPartsFiles ( + parts: gmail_v1.Schema$MessagePart[] | undefined, + messageId: string + ): Promise { + const result: AttachedFile[] = [] + if (parts !== undefined) { + const filtredParts = parts.filter((part) => part.filename != null && part.filename.length > 0) + for (const part of filtredParts ?? []) { + const res = await this.getPartFiles(part, messageId) + if (res.length > 0) { + result.push(...res) + } + } + } + return result + } + + private createTx ( + message: AttachedData & { modifiedOn: Timestamp }, + factory: TxFactory, + channel: Channel + ): TxCollectionCUD { + const tx = factory.createTxCollectionCUD( + channel._class, + channel._id, + channel.space, + 'items', + factory.createTxCreateDoc(gmail.class.Message, core.space.Workspace, message as unknown as Data) + ) + tx.tx.modifiedOn = message.modifiedOn + return tx + } + + private updateTx ( + message: AttachedData & { modifiedOn: Timestamp }, + current: Message, + factory: TxFactory, + channel: Channel + ): TxCollectionCUD | undefined { + const operations = diffAttributes(current, message) + if (Object.keys(operations).length === 0) return undefined + const tx = factory.createTxCollectionCUD( + channel._class, + channel._id, + channel.space, + 'items', + factory.createTxUpdateDoc(current._class, current.space, current._id, operations) + ) + return tx + } + + private async refreshToken (): Promise { + try { + clearTimeout(this.refreshTimer) + const res = await this.oAuth2Client.refreshAccessToken() + await this.updateToken(res.credentials) + this.refreshTimer = setTimeout( + () => { + void this.refreshToken() + }, + 30 * 60 * 1000 + ) + } catch (err: any) { + console.log("Couldn't refresh token, error:", JSON.stringify(err)) + if (err?.response?.data?.error === 'invalid_grant') { + await this.workspace.signoutByUserId(this.user.userId, true) + } else { + this.refreshTimer = setTimeout( + () => { + void this.refreshToken() + }, + 15 * 60 * 1000 + ) + } + throw err + } + } + + private async watch (): Promise { + try { + await this.rateLimiter.take(100) + await this.gmail.watch({ + userId: 'me', + requestBody: { + topicName: config.WATCH_TOPIC_NAME + } + }) + } catch (err) { + console.log('Watch error', JSON.stringify(err)) + } + } + + private async makeAttachmentsBody (message: NewMessage, from: string): Promise { + const str = [ + 'Content-Type: multipart/mixed; boundary="mail"\n', + 'MIME-Version: 1.0\n', + `To: ${message.to} \n`, + `From: ${from} \n` + ] + + if (message.replyTo != null) { + str.push(`In-Reply-To: ${message.replyTo} \n`) + } + + if (message.copy != null && message.copy.length > 0) { + str.push(`Cc: ${message.copy.join(', ')} \n`) + } + + if (message.subject != null) { + str.push(`Subject: =?UTF-8?B?${encode64(message.subject)}?= \n`) + } + + str.push('\n\n') + str.push('--mail\n') + str.push('Content-Type: text/html; charset="UTF-8"\n') + str.push('MIME-Version: 1.0\n') + str.push('Content-Transfer-Encoding: 7bit\n\n') + str.push(addFooter(message.content)) + str.push('\n\n') + + const attachments = await this.client.findAll(attachment.class.Attachment, { attachedTo: message._id }) + + for (const attachment of attachments) { + const attachmentStrings = await this.makeAttachmentPart(attachment) + str.push(...attachmentStrings) + } + + str.push('--mail--') + + return encode64(str.join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') + } + + private async makeAttachmentPart (attachment: Attachment): Promise { + const data = await this.getAttachmentData(attachment.file) + const res: string[] = [] + res.push('--mail\n') + res.push(`Content-Type: ${attachment.type}\n`) + res.push('MIME-Version: 1.0\n') + res.push('Content-Transfer-Encoding: base64\n') + res.push(`Content-Disposition: attachment; filename="${attachment.name}"\n\n`) + res.push(data) + res.push('\n\n') + return res + } + + private async getAttachmentData (fileId: string): Promise { + const uploadUrl = config.UploadUrl + if (uploadUrl === undefined) { + throw Error('UploadURL is not defined') + } + const url = `${uploadUrl}?file=${fileId}&token=${this.user.token}` + const res = await fetch(url) + return arrayBufferToBase64(await res.arrayBuffer()) + } + + async close (): Promise { + if (this.watchTimer !== undefined) clearInterval(this.watchTimer) + if (this.refreshTimer !== undefined) clearTimeout(this.refreshTimer) + try { + await this.rateLimiter.take(50) + await this.gmail.stop({ + userId: 'me' + }) + } catch (err) { + console.log('close error', JSON.stringify(err)) + } + } +} diff --git a/services/gmail/pod-gmail/src/gmailController.ts b/services/gmail/pod-gmail/src/gmailController.ts new file mode 100644 index 0000000000..acd0b827d5 --- /dev/null +++ b/services/gmail/pod-gmail/src/gmailController.ts @@ -0,0 +1,120 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Db } from 'mongodb' +import { decode64 } from './base64' +import config from './config' +import { type GmailClient } from './gmail' +import { type ProjectCredentials, type Token, type User } from './types' +import { WorkspaceClient } from './workspaceClient' + +export class GmailController { + private readonly workspaces: Map = new Map() + + private readonly credentials: ProjectCredentials + private readonly clients: Map = new Map() + + protected static _instance: GmailController + + private constructor (private readonly mongo: Db) { + this.credentials = JSON.parse(config.Credentials) + GmailController._instance = this + } + + static getGmailController (mongo?: Db): GmailController { + if (GmailController._instance !== undefined) { + return GmailController._instance + } + if (mongo === undefined) throw new Error('GmailController not exist') + return new GmailController(mongo) + } + + async startAll (): Promise { + const tokens = await this.mongo.collection('tokens').find().toArray() + for (const token of tokens) { + try { + await this.createClient(token) + } catch (err) { + console.error(`Couldn't create client for ${token.workspace} ${token.userId}`) + } + } + + for (const client of this.workspaces.values()) { + void client.checkUsers().then(async () => { + await client.getNewMessages() + }) + } + } + + push (message: string): void { + const data = JSON.parse(decode64(message)) + const email = data.emailAddress + const clients = this.clients.get(email) + for (const client of clients ?? []) { + void client.sync() + } + } + + addClient (email: string, client: GmailClient): void { + const clients = this.clients.get(email) + if (clients === undefined) { + this.clients.set(email, [client]) + } else { + clients.push(client) + this.clients.set(email, clients) + } + } + + async getGmailClient (email: string, workspace: string, token: string): Promise { + const workspaceClient = await this.getWorkspaceClient(workspace) + const userId = await workspaceClient.getUserId(email) + return await workspaceClient.createGmailClient({ userId, workspace, token }) + } + + async signout (workspace: string, email: string): Promise { + const workspaceClient = await this.getWorkspaceClient(workspace) + const clients = await workspaceClient.signout(email) + if (clients === 0) { + this.workspaces.delete(workspace) + } + } + + async close (): Promise { + for (const workspace of this.workspaces.values()) { + await workspace.close() + } + this.workspaces.clear() + } + + async createClient (user: User | Token): Promise { + const workspace = await this.getWorkspaceClient(user.workspace) + const newClient = await workspace.createGmailClient(user) + return newClient + } + + private async getWorkspaceClient (workspace: string): Promise { + let res = this.workspaces.get(workspace) + if (res === undefined) { + try { + res = await WorkspaceClient.create(this.credentials, this.mongo, workspace) + this.workspaces.set(workspace, res) + } catch (err) { + console.error(`Couldn't create workspace worker for ${workspace}, reason: ${JSON.stringify(err)}`) + throw err + } + } + return res + } +} diff --git a/services/gmail/pod-gmail/src/index.ts b/services/gmail/pod-gmail/src/index.ts new file mode 100644 index 0000000000..5096d74da9 --- /dev/null +++ b/services/gmail/pod-gmail/src/index.ts @@ -0,0 +1,21 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { main } from './main' + +// eslint-disable-next-line +require('dotenv').config() +void main() diff --git a/services/gmail/pod-gmail/src/jwt.ts b/services/gmail/pod-gmail/src/jwt.ts new file mode 100644 index 0000000000..b7e42523b7 --- /dev/null +++ b/services/gmail/pod-gmail/src/jwt.ts @@ -0,0 +1,22 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { encode as jwtEncode, decode as jwtDecode } from 'jwt-simple' + +import config from './config' + +export const encode = (data: any): string => jwtEncode(data, config.Secret) +export const decode = (data: string): any => jwtDecode(data, config.Secret) diff --git a/services/gmail/pod-gmail/src/main.ts b/services/gmail/pod-gmail/src/main.ts new file mode 100644 index 0000000000..27d9f7c0f7 --- /dev/null +++ b/services/gmail/pod-gmail/src/main.ts @@ -0,0 +1,135 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { setMetadata } from '@hcengineering/platform' +import serverClient from '@hcengineering/server-client' +import { type IncomingHttpHeaders } from 'http' +import { decode64 } from './base64' +import config from './config' +import { GmailController } from './gmailController' +import { decode } from './jwt' +import { createServer, listen } from './server' +import { closeDB, getDB } from './storage' +import { type Endpoint, type State } from './types' + +const extractToken = (header: IncomingHttpHeaders): any => { + try { + return header.authorization?.slice(7) ?? '' + } catch { + return undefined + } +} + +export const main = async (): Promise => { + setMetadata(serverClient.metadata.Endpoint, config.AccountsURL) + setMetadata(serverClient.metadata.UserAgent, config.ServiceID) + const db = await getDB() + const gmailController = GmailController.getGmailController(db) + await gmailController.startAll() + const endpoints: Endpoint[] = [ + { + endpoint: '/signin', + type: 'get', + handler: async (req, res) => { + try { + const token = extractToken(req.headers) + + if (token === undefined) { + res.status(401).send() + return + } + const redirectURL = req.query.redirectURL as string + + const { email, workspace } = decode(token) + const gmail = await gmailController.getGmailClient(email, workspace, token) + const url = gmail.getAutUrl(redirectURL) + res.send(url) + } catch (err) { + console.log('signin error', JSON.stringify(err)) + res.status(500).send() + } + } + }, + { + endpoint: '/signin/code', + type: 'get', + handler: async (req, res) => { + const code = req.query.code as string + const state = JSON.parse(decode64(req.query.state as string)) as unknown as State + const gmail = await gmailController.createClient(state) + await gmail.authorize(code) + res.redirect(state.redirectURL) + } + }, + { + endpoint: '/signout', + type: 'get', + handler: async (req, res) => { + try { + const token = extractToken(req.headers) + + if (token === undefined) { + res.status(401).send() + return + } + + const { email, workspace } = decode(token) + await gmailController.signout(workspace, email) + } catch (err) { + console.log('signout error', JSON.stringify(err)) + } + + res.send() + } + }, + { + endpoint: '/push', + type: 'post', + handler: async (req, res) => { + const data = req.body?.message?.data + if (data === undefined) { + res.status(400).send({ err: "'data' is missing" }) + return + } + gmailController.push(data) + + res.send() + } + } + ] + + const server = listen(createServer(endpoints), config.Port) + + const shutdown = (): void => { + server.close(() => { + void gmailController + .close() + .then(async () => { + await closeDB() + }) + .then(() => process.exit()) + }) + } + + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + process.on('uncaughtException', (e) => { + console.error(e) + }) + process.on('unhandledRejection', (e) => { + console.error(e) + }) +} diff --git a/services/gmail/pod-gmail/src/rateLimiter.ts b/services/gmail/pod-gmail/src/rateLimiter.ts new file mode 100644 index 0000000000..ea4a986ec4 --- /dev/null +++ b/services/gmail/pod-gmail/src/rateLimiter.ts @@ -0,0 +1,34 @@ +export class RateLimiter { + private tokens: number + private lastRefillTime: number + + constructor ( + private readonly refillInterval: number, + private readonly maxTokens: number + ) { + this.tokens = maxTokens + this.lastRefillTime = Date.now() + } + + private refillTokens (): void { + const now = Date.now() + const elapsedTime = now - this.lastRefillTime + const tokensToAdd = (elapsedTime / this.refillInterval) * this.maxTokens + + this.tokens = Math.min(this.tokens + tokensToAdd, this.maxTokens) + this.lastRefillTime = now + } + + public async take (count: number): Promise { + if (count > this.maxTokens) throw new Error('Cannot take more tokens than the maximum allowed') + this.refillTokens() + + while (this.tokens < count) { + // Wait for the next refill + await new Promise((resolve) => setTimeout(resolve, this.refillInterval)) + this.refillTokens() + } + + this.tokens -= count + } +} diff --git a/services/gmail/pod-gmail/src/server.ts b/services/gmail/pod-gmail/src/server.ts new file mode 100644 index 0000000000..f171ec02b9 --- /dev/null +++ b/services/gmail/pod-gmail/src/server.ts @@ -0,0 +1,69 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import cors from 'cors' +import express, { type Express, type NextFunction, type Request, type Response } from 'express' +import { type Server } from 'http' +import { ApiError } from './error' +import { type Endpoint, type RequestHandler } from './types' + +const catchError = (fn: RequestHandler) => (req: Request, res: Response, next: NextFunction) => { + void (async () => { + try { + await fn(req, res, next) + } catch (err: unknown) { + next(err) + } + })() +} + +export function createServer (endpoints: Endpoint[]): Express { + const app = express() + + app.use(cors()) + app.use(express.json()) + + endpoints.forEach((endpoint) => { + if (endpoint.type === 'get') { + app.get(endpoint.endpoint, catchError(endpoint.handler)) + } else if (endpoint.type === 'post') { + app.post(endpoint.endpoint, catchError(endpoint.handler)) + } + }) + + app.use((_req, res, _next) => { + res.status(404).send({ message: 'Not found' }) + }) + + app.use((err: any, _req: any, res: any, _next: any) => { + if (err instanceof ApiError) { + res.status(400).send({ code: err.code, message: err.message }) + return + } + + res.status(500).send({ message: err.message }) + }) + + return app +} + +export function listen (e: Express, port: number, host?: string): Server { + const cb = (): void => { + console.log(`Gmail service has been started at ${host ?? '*'}:${port}`) + } + + return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb) +} diff --git a/services/gmail/pod-gmail/src/storage.ts b/services/gmail/pod-gmail/src/storage.ts new file mode 100644 index 0000000000..972742cbfd --- /dev/null +++ b/services/gmail/pod-gmail/src/storage.ts @@ -0,0 +1,37 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MongoClient } from 'mongodb' + +import config from './config' + +let client: MongoClient | undefined +export const getDB = (() => { + return async () => { + if (client === undefined) { + client = new MongoClient(config.MongoURI) + await client.connect() + } + + return client.db(config.MongoDB) + } +})() + +export const closeDB: () => Promise = async () => { + if (client !== undefined) { + await client.close() + } +} diff --git a/services/gmail/pod-gmail/src/types.ts b/services/gmail/pod-gmail/src/types.ts new file mode 100644 index 0000000000..ca7818d901 --- /dev/null +++ b/services/gmail/pod-gmail/src/types.ts @@ -0,0 +1,70 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Account, Doc, Ref } from '@hcengineering/core' +import type { NextFunction, Request, Response } from 'express' +import type { Credentials } from 'google-auth-library' +import type { Channel as PlatformChannel } from '@hcengineering/contact' + +export type Token = User & Credentials + +export type History = User & { + historyId: string +} + +export interface User { + userId: Ref + workspace: string + token: string +} + +export type State = User & { + redirectURL: string +} + +export interface AttachedFile { + size?: number + file: string + type?: string + lastModified: number + name: string +} + +export type Channel = Pick + +export type RequestType = 'get' | 'post' + +export type RequestHandler = (req: Request, res: Response, next?: NextFunction) => Promise + +export interface Endpoint { + endpoint: string + type: RequestType + handler: RequestHandler +} + +export interface ProjectCredentials { + web: ProjectCredentialsData +} + +export interface ProjectCredentialsData { + client_id: string + project_id: string + auth_uri: string + token_uri: string + auth_provider_x509_cert_url: string + client_secret: string + redirect_uris: string[] +} diff --git a/services/gmail/pod-gmail/src/utils.ts b/services/gmail/pod-gmail/src/utils.ts new file mode 100644 index 0000000000..193fd39860 --- /dev/null +++ b/services/gmail/pod-gmail/src/utils.ts @@ -0,0 +1,65 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Data, type Doc, type DocumentUpdate } from '@hcengineering/core' +import { deepEqual } from 'fast-equals' +import { type Token, type User } from './types' + +export class DeferredPromise { + public readonly promise: Promise + private _resolve: (x: T) => void = () => {} + private _reject: (err: any) => void = () => {} + + constructor () { + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve + this._reject = reject + }) + } + + get resolve (): typeof this._resolve { + return this._resolve + } + + get reject (): typeof this._reject { + return this._reject + } +} + +export function diffAttributes (doc: Data, newDoc: Data): DocumentUpdate { + const result: DocumentUpdate = {} + const allDocuments = new Map(Object.entries(doc)) + const newDocuments = new Map(Object.entries(newDoc)) + const targetKeys = ['from', 'textContent', 'replyTo', 'to', 'subject', 'content', 'copy', 'incoming', 'sendOn'] + + for (const [key, value] of allDocuments) { + if (!targetKeys.includes(key)) continue + const newValue = toUndef(newDocuments.get(key)) + if (!deepEqual(newValue, toUndef(value)) && newValue !== undefined && newValue !== null) { + // update is required, since values are different + result[key] = newValue + } + } + return result +} + +function toUndef (value: any): any { + return value === null ? undefined : value +} + +export function isToken (user: User | Token): user is Token { + return (user as Token).access_token !== undefined +} diff --git a/services/gmail/pod-gmail/src/workspaceClient.ts b/services/gmail/pod-gmail/src/workspaceClient.ts new file mode 100644 index 0000000000..8f0a46af00 --- /dev/null +++ b/services/gmail/pod-gmail/src/workspaceClient.ts @@ -0,0 +1,353 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import contact, { type Employee, type PersonAccount, type Channel as PlatformChannel } from '@hcengineering/contact' +import core, { + type Account, + type AttachedDoc, + type Client, + type Doc, + type Ref, + type Tx, + type TxCollectionCUD, + type TxCreateDoc, + TxProcessor, + type TxRemoveDoc, + type TxUpdateDoc +} from '@hcengineering/core' +import gmailP, { type NewMessage } from '@hcengineering/gmail' +import { type Db } from 'mongodb' +import { getClient } from './client' +import config from './config' +import { GmailClient } from './gmail' +import { encode } from './jwt' +import { type ProjectCredentials, type User, type Channel } from './types' + +export class WorkspaceClient { + private messageSubscribed: boolean = false + private channels: Map = new Map() + private channelsById: Map, Channel> = new Map, Channel>() + private readonly txHandlers: ((...tx: Tx[]) => Promise)[] = [] + + private client!: Client + private readonly clients: Map, GmailClient> = new Map, GmailClient>() + + private constructor ( + private readonly credentials: ProjectCredentials, + private readonly mongo: Db, + private readonly workspace: string + ) {} + + static async create (credentials: ProjectCredentials, mongo: Db, workspace: string): Promise { + const instance = new WorkspaceClient(credentials, mongo, workspace) + await instance.initClient(workspace) + return instance + } + + async createGmailClient (user: User): Promise { + const current = this.getGmailClient(user.userId) + if (current !== undefined) return current + const newClient = await GmailClient.create(this.credentials, user, this.mongo, this.client, this) + this.clients.set(user.userId, newClient) + return newClient + } + + async close (): Promise { + for (const client of this.clients.values()) { + await client.close() + } + this.clients.clear() + await this.client?.close() + } + + async getUserId (email: string): Promise> { + const user = await this.client.findOne(core.class.Account, { + email + }) + if (user === undefined) { + throw new Error('User not found') + } + return user._id + } + + async signout (email: string, byError: boolean = false): Promise { + const userId = await this.getUserId(email) + const client = this.clients.get(userId) + if (client !== undefined) { + await client.signout(byError) + } + this.clients.delete(userId) + return this.clients.size + } + + async signoutByUserId (userId: Ref, byError: boolean = false): Promise { + const client = this.clients.get(userId) + if (client !== undefined) { + await client.signout(byError) + } + this.clients.delete(userId) + return this.clients.size + } + + private getGmailClient (userId: Ref): GmailClient | undefined { + return this.clients.get(userId) + } + + private async initClient (workspace: string): Promise { + const token = encode({ + email: config.SystemEmail, + workspace + }) + const client = await getClient(token) + client.notify = (...tx: Tx[]) => { + void this.txHandler(...tx) + } + + this.client = client + await this.setChannels() + return this.client + } + + private async txHandler (...tx: Tx[]): Promise { + await Promise.all( + this.txHandlers.map(async (handler) => { + await handler(...tx) + }) + ) + } + + // #region Message + + async subscribeMessages (): Promise { + if (this.messageSubscribed) return + this.txHandlers.push(async (...txes: Tx[]) => { + for (const tx of txes) { + await this.txMessageHandler(tx) + } + }) + this.messageSubscribed = true + } + + async getNewMessages (): Promise { + const newMessages = await this.client.findAll(gmailP.class.NewMessage, { + status: 'new' + }) + await this.subscribeMessages() + for (const message of newMessages) { + const client = this.getGmailClient(message.from ?? message.createdBy ?? message.modifiedBy) + if (client !== undefined) { + await client.createMessage(message) + } + } + } + + private async txMessageHandler (tx: Tx): Promise { + switch (tx._class) { + case core.class.TxCreateDoc: { + await this.txCreateMessage(tx as TxCreateDoc) + break + } + case core.class.TxUpdateDoc: { + await this.txUpdateMessage(tx as TxUpdateDoc) + break + } + } + } + + private async txCreateMessage (tx: TxCreateDoc): Promise { + const hierarchy = this.client.getHierarchy() + if (hierarchy.isDerived(tx.objectClass, gmailP.class.NewMessage)) { + const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc) + await this.prepareAndSendMessage(doc) + } + } + + private async txUpdateMessage (tx: TxUpdateDoc): Promise { + const hierarchy = this.client.getHierarchy() + if (hierarchy.isDerived(tx.objectClass, gmailP.class.NewMessage)) { + if (tx.operations?.status !== 'new') return + const doc = await this.client.findOne(tx.objectClass, { _id: tx.objectId }) + if (doc === undefined) return + await this.prepareAndSendMessage(doc) + } + } + + private async prepareAndSendMessage (doc: NewMessage): Promise { + const client = this.getGmailClient(doc.from ?? doc.createdBy ?? doc.modifiedBy) + if (client === undefined) { + return + } + await client.createMessage(doc) + } + + // #endregion + + // #region channels + + getChannel (channel: string): Channel | undefined { + return this.channels.get(normalize(channel)) + } + + private async setChannels (): Promise { + const channels = await this.getChannels(this.client) + this.channels = new Map( + channels.map((p) => { + return [normalize(p.value), p] + }) + ) + this.channelsById = new Map( + channels.map((p) => { + return [p._id, p] + }) + ) + + this.txHandlers.push(async (...txes: Tx[]) => { + for (const tx of txes) { + await this.txChannelHandler(tx) + } + }) + } + + private async getChannels (client: Client): Promise { + return await client.findAll( + contact.class.Channel, + { + provider: contact.channelProvider.Email + }, + { projection: { _id: 1, _class: 1, space: 1, value: 1 } } + ) + } + + private async addChannel (channel: Channel): Promise { + this.channels.set(normalize(channel.value), channel) + this.channelsById.set(channel._id, channel) + } + + private async syncChannel (channel: Channel): Promise { + await Promise.all( + Array.from(this.clients.values()).map(async (p) => { + await p.newChannel(channel.value) + }) + ) + } + + private async removeChannel (channel: Channel): Promise { + this.channels.delete(normalize(channel.value)) + this.channelsById.delete(channel._id) + } + + private async txChannelHandler (tx: Tx): Promise { + switch (tx._class) { + case core.class.TxCreateDoc: { + await this.txCreateChannel(tx as TxCreateDoc) + return + } + case core.class.TxCollectionCUD: { + await this.txCollectionCUD(tx as TxCollectionCUD) + return + } + case core.class.TxUpdateDoc: { + await this.txUpdateChannel(tx as TxUpdateDoc) + return + } + case core.class.TxRemoveDoc: { + await this.txRemoveChannel(tx as TxRemoveDoc) + } + } + } + + private async txCollectionCUD (tx: TxCollectionCUD): Promise { + await this.txHandler(tx.tx) + } + + private async txCreateChannel (tx: TxCreateDoc): Promise { + const hierarhy = this.client.getHierarchy() + if (hierarhy.isDerived(tx.objectClass, contact.class.Channel)) { + const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc) + if (doc.provider === contact.channelProvider.Email) { + await this.addChannel(doc) + await this.syncChannel(doc) + } + } + } + + private async txUpdateChannel (tx: TxUpdateDoc): Promise { + const hierarhy = this.client.getHierarchy() + if (hierarhy.isDerived(tx.objectClass, contact.class.Channel)) { + const ctx = tx as TxUpdateDoc + const channel = this.channelsById.get(ctx.objectId) + if (channel !== undefined && ctx.operations.value !== undefined) { + await this.removeChannel(channel) + channel.value = ctx.operations.value + await this.addChannel(channel) + await this.syncChannel(channel) + } + } + } + + private async txRemoveChannel (tx: TxRemoveDoc): Promise { + const hierarhy = this.client.getHierarchy() + if (hierarhy.isDerived(tx.objectClass, contact.class.Channel)) { + const ctx = tx as TxRemoveDoc + const channel = this.channelsById.get(ctx.objectId) + if (channel !== undefined) { + await this.removeChannel(channel) + } + } + } + + // #endregion + + // #region Users + + async checkUsers (): Promise { + const removedEmployees = await this.client.findAll(contact.mixin.Employee, { + active: false + }) + const accounts = await this.client.findAll(contact.class.PersonAccount, { + person: { $in: removedEmployees.map((p) => p._id) } + }) + for (const acc of accounts) { + await this.deactivateUser(acc) + } + this.txHandlers.push(async (...txes: Tx[]) => { + for (const tx of txes) { + await this.txEmployeeHandler(tx) + } + }) + } + + private async deactivateUser (acc: PersonAccount): Promise { + await this.signout(acc.email, true) + } + + private async txEmployeeHandler (tx: Tx): Promise { + if (tx._class !== core.class.TxUpdateDoc) return + const ctx = tx as TxUpdateDoc + if (!this.client.getHierarchy().isDerived(ctx.objectClass, contact.mixin.Employee)) return + if (ctx.operations.active === false) { + const acc = await this.client.findOne(contact.class.PersonAccount, { person: ctx.objectId }) + if (acc !== undefined) { + await this.deactivateUser(acc) + } + } + } + + // #endregion +} + +function normalize (str: string): string { + return str.toLowerCase().trim() +} diff --git a/services/gmail/pod-gmail/tsconfig.json b/services/gmail/pod-gmail/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/gmail/pod-gmail/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/love/.eslintrc.js b/services/love/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/love/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/love/.npmignore b/services/love/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/love/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/love/Dockerfile b/services/love/Dockerfile new file mode 100644 index 0000000000..3d6a6bdb8f --- /dev/null +++ b/services/love/Dockerfile @@ -0,0 +1,10 @@ + +FROM node:20 + +WORKDIR /usr/src/app +RUN npm install --ignore-scripts=false --verbose bufferutil utf-8-validate @mongodb-js/zstd snappy --unsafe-perm + +COPY bundle/bundle.js ./ + +EXPOSE 8096 +CMD [ "node", "bundle.js" ] diff --git a/services/love/config/rig.json b/services/love/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/love/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/love/jest.config.js b/services/love/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/love/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/love/package.json b/services/love/package.json new file mode 100644 index 0000000000..8a6f781c8a --- /dev/null +++ b/services/love/package.json @@ -0,0 +1,73 @@ +{ + "name": "@hcengineering/pod-love", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "_phase:bundle": "rushx bundle", + "_phase:docker-build": "rushx docker:build", + "_phase:docker-staging": "rushx docker:staging", + "bundle": "mkdir -p bundle && esbuild src/index.ts --bundle --platform=node > bundle/bundle.js", + "docker:build": "../../common/scripts/docker_build.sh hardcoreeng/love .", + "docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/love staging", + "docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/love", + "run-local": "cross-env ts-node src/index.ts", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@tsconfig/node16": "^1.0.4", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/node": "~20.11.16", + "@types/ws": "^8.5.11", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@hcengineering/platform-rig": "^0.6.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/uuid": "^8.3.1", + "@typescript-eslint/parser": "^6.11.0", + "esbuild": "^0.20.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "prettier": "^3.1.0", + "ts-node": "^10.8.0", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/love": "^0.6.0", + "@hcengineering/drive": "^0.6.0", + "@hcengineering/core": "^0.6.32", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/server-storage": "^0.6.0", + "@hcengineering/client": "^0.6.18", + "@hcengineering/client-resources": "^0.6.27", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/server-client": "^0.6.0", + "ws": "^8.18.0", + "livekit-server-sdk": "^2.0.10", + "jwt-simple": "^0.5.6", + "uuid": "^8.3.2", + "dotenv": "~16.0.0", + "cors": "^2.8.5", + "express": "^4.19.2" + } +} diff --git a/services/love/src/client.ts b/services/love/src/client.ts new file mode 100644 index 0000000000..a6881b06dd --- /dev/null +++ b/services/love/src/client.ts @@ -0,0 +1,23 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Client } from '@hcengineering/core' +import { createClient, getTransactorEndpoint } from '@hcengineering/server-client' + +export async function getClient (token: string): Promise { + const endpoint = await getTransactorEndpoint(token) + return await createClient(endpoint, token) +} diff --git a/services/love/src/config.ts b/services/love/src/config.ts new file mode 100644 index 0000000000..aaab341ed8 --- /dev/null +++ b/services/love/src/config.ts @@ -0,0 +1,80 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +interface Config { + AccountsURL: string + Port: number + SystemEmail: string + ServiceID: string + + LiveKitHost: string + ApiKey: string + ApiSecret: string + + StorageConfig: string + StorageProviderName: string + Secret: string + + MongoUrl: string +} + +const envMap: { [key in keyof Config]: string } = { + AccountsURL: 'ACCOUNTS_URL', + Port: 'PORT', + + LiveKitHost: 'LIVEKIT_HOST', + ApiKey: 'LIVEKIT_API_KEY', + ApiSecret: 'LIVEKIT_API_SECRET', + + StorageConfig: 'STORAGE_CONFIG', + StorageProviderName: 'STORAGE_PROVIDER_NAME', + Secret: 'SECRET', + ServiceID: 'SERVICE_ID', + SystemEmail: 'SYSTEM_EMAIL', + MongoUrl: 'MONGO_URL' +} + +const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined) + +const config: Config = (() => { + const params: Partial = { + AccountsURL: process.env[envMap.AccountsURL], + Port: parseNumber(process.env[envMap.Port]) ?? 8096, + LiveKitHost: process.env[envMap.LiveKitHost], + ApiKey: process.env[envMap.ApiKey], + ApiSecret: process.env[envMap.ApiSecret], + StorageConfig: process.env[envMap.StorageConfig], + StorageProviderName: process.env[envMap.StorageProviderName] ?? 's3', + Secret: process.env[envMap.Secret], + ServiceID: process.env[envMap.ServiceID] ?? 'love-service', + SystemEmail: process.env[envMap.SystemEmail] ?? 'anticrm@hc.engineering', + MongoUrl: process.env[envMap.MongoUrl] + } + + const optional = ['StorageConfig'] + + const missingEnv = (Object.keys(params) as Array) + .filter((key) => !optional.includes(key)) + .filter((key) => params[key] === undefined) + .map((key) => envMap[key]) + + if (missingEnv.length > 0) { + throw Error(`Missing env variables: ${missingEnv.join(', ')}`) + } + + return params as Config +})() + +export default config diff --git a/services/love/src/index.ts b/services/love/src/index.ts new file mode 100644 index 0000000000..56a945a7fb --- /dev/null +++ b/services/love/src/index.ts @@ -0,0 +1,21 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// eslint-disable-next-line +require('dotenv').config() +// eslint-disable-next-line +const { main } = require('./main') + +void main() diff --git a/services/love/src/jwt.ts b/services/love/src/jwt.ts new file mode 100644 index 0000000000..a83e4c3001 --- /dev/null +++ b/services/love/src/jwt.ts @@ -0,0 +1,21 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { encode as jwtEncode, decode as jwtDecode } from 'jwt-simple' + +import config from './config' + +export const encode = (data: any): string => jwtEncode(data, config.Secret) +export const decode = (data: string): any => jwtDecode(data, config.Secret) diff --git a/services/love/src/main.ts b/services/love/src/main.ts new file mode 100644 index 0000000000..699a1e5a98 --- /dev/null +++ b/services/love/src/main.ts @@ -0,0 +1,241 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MeasureMetricsContext, WorkspaceId, getWorkspaceId, newMetrics, toWorkspaceString } from '@hcengineering/core' +import { setMetadata } from '@hcengineering/platform' +import serverClient from '@hcengineering/server-client' +import { StorageConfig, StorageConfiguration } from '@hcengineering/server-core' +import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage' +import cors from 'cors' +import express from 'express' +import { IncomingHttpHeaders } from 'http' +import { + AccessToken, + EgressClient, + EncodedFileOutput, + EncodedFileType, + RoomServiceClient, + S3Upload, + WebhookReceiver +} from 'livekit-server-sdk' +import { v4 as uuid } from 'uuid' +import config from './config' +import { decode } from './jwt' +import { WorkspaceClient } from './workspaceClient' + +const extractToken = (header: IncomingHttpHeaders): any => { + try { + return header.authorization?.slice(7) ?? '' + } catch { + return undefined + } +} + +export const main = async (): Promise => { + setMetadata(serverClient.metadata.Endpoint, config.AccountsURL) + setMetadata(serverClient.metadata.UserAgent, config.ServiceID) + + const storageConfigs: StorageConfiguration = storageConfigFromEnv() + const ctx = new MeasureMetricsContext('love', {}, {}, newMetrics()) + const storageConfig = storageConfigs.storages.findLast((p) => p.name === config.StorageProviderName) + const storageAdapter = buildStorageFromConfig(storageConfigs, config.MongoUrl) + const app = express() + const port = config.Port + app.use(cors()) + app.use(express.raw({ type: 'application/webhook+json' })) + app.use(express.json()) + + const receiver = new WebhookReceiver(config.ApiKey, config.ApiSecret) + const roomClient = new RoomServiceClient(config.LiveKitHost, config.ApiKey, config.ApiSecret) + const egressClient = new EgressClient(config.LiveKitHost, config.ApiKey, config.ApiSecret) + const dataByUUID = new Map< + string, + { + name: string + workspace: string + workspaceId: WorkspaceId + } + >() + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + app.post('/webhook', async (req, res) => { + try { + const event = await receiver.receive(req.body, req.get('Authorization')) + if (event.event === 'egress_ended' && event.egressInfo !== undefined) { + for (const res of event.egressInfo.fileResults) { + const data = dataByUUID.get(res.filename) + if (data !== undefined) { + const client = await WorkspaceClient.create(data.workspace) + await storageAdapter.syncBlobFromStorage(ctx, data.workspaceId, res.filename, storageConfig?.name) + await client.saveFile(res.filename, data.name) + await client.close() + dataByUUID.delete(res.filename) + } else { + console.log('no data found for', res.filename) + } + } + res.send() + return + } + res.status(400).send() + } catch (e) { + console.error(e) + res.status(500).send() + } + }) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + app.post('/getToken', async (req, res) => { + const roomName = req.body.roomName + const _id = req.body._id + const participantName = req.body.participantName + res.send(await createToken(roomName, _id, participantName)) + }) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + app.get('/checkRecordAvailable', async (_req, res) => { + res.send(await checkRecordAvailable(storageConfig)) + }) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + app.post('/startRecord', async (req, res) => { + const token = extractToken(req.headers) + + if (token === undefined) { + res.status(401).send() + return + } + + const roomName = req.body.roomName + const room = req.body.room + const { workspace, productId } = decode(token) + const workspaceId = getWorkspaceId(workspace, productId) + + try { + const dateStr = new Date().toISOString().replace('T', '_').slice(0, 19) + const name = `${room}_${dateStr}.mp4` + const id = await startRecord(storageConfig, egressClient, roomClient, roomName, workspaceId) + dataByUUID.set(id, { name, workspace, workspaceId }) + res.send() + } catch (e) { + console.error(e) + res.status(500).send() + } + }) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + app.post('/stopRecord', async (req, res) => { + const token = extractToken(req.headers) + + if (token === undefined) { + res.status(401).send() + return + } + // just check token + decode(token) + await roomClient.updateRoomMetadata(req.body.roomName, JSON.stringify({ recording: false })) + void stopEgress(egressClient, req.body.roomName) + res.send() + }) + + const server = app.listen(port, () => { + console.log(`Server listening on port ${port}`) + }) + + const shutdown = (): void => { + server.close(() => process.exit()) + } + + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + process.on('uncaughtException', (e) => { + console.error(e) + }) + process.on('unhandledRejection', (e) => { + console.error(e) + }) +} + +const stopEgress = async (egressClient: EgressClient, roomName: string): Promise => { + const egresses = await egressClient.listEgress({ active: true, roomName }) + for (const egress of egresses) { + await egressClient.stopEgress(egress.egressId) + } +} + +const createToken = async (roomName: string, _id: string, participantName: string): Promise => { + const at = new AccessToken(config.ApiKey, config.ApiSecret, { + identity: _id, + name: participantName, + // token to expire after 10 minutes + ttl: '10m' + }) + at.addGrant({ roomJoin: true, room: roomName }) + + return await at.toJwt() +} + +const checkRecordAvailable = async (storageConfig: StorageConfig | undefined): Promise => { + return storageConfig !== undefined +} + +function getBucket (storageConfig: any, workspaceId: WorkspaceId): string { + return storageConfig.rootBucket ?? (storageConfig.bucketPrefix ?? '') + toWorkspaceString(workspaceId, '.') +} + +function getBucketFolder (workspaceId: WorkspaceId): string { + return toWorkspaceString(workspaceId, '.') +} + +function getDocumentKey (storageConfig: any, workspace: WorkspaceId, name: string): string { + return storageConfig.rootBucket === undefined ? name : `${getBucketFolder(workspace)}/${name}` +} + +const startRecord = async ( + storageConfig: StorageConfig | undefined, + egressClient: EgressClient, + roomClient: RoomServiceClient, + roomName: string, + workspaceId: WorkspaceId +): Promise => { + if (storageConfig === undefined) { + console.error('please provide s3 storage configuration') + throw new Error('please provide s3 storage configuration') + } + const endpoint = storageConfig.endpoint + const accessKey = (storageConfig as any).accessKey + const secret = (storageConfig as any).secretKey + const region = (storageConfig as any).region ?? 'auto' + const bucket = getBucket(storageConfig, workspaceId) + const name = uuid() + const filepath = getDocumentKey(storageConfig, workspaceId, `${name}.mp4`) + const output = new EncodedFileOutput({ + fileType: EncodedFileType.MP4, + filepath, + output: { + case: 's3', + value: new S3Upload({ + endpoint, + accessKey, + region, + secret, + bucket + }) + } + }) + await roomClient.updateRoomMetadata(roomName, JSON.stringify({ recording: true })) + await egressClient.startRoomCompositeEgress(roomName, { file: output }, { layout: 'grid' }) + return filepath +} diff --git a/services/love/src/workspaceClient.ts b/services/love/src/workspaceClient.ts new file mode 100644 index 0000000000..0048edabf1 --- /dev/null +++ b/services/love/src/workspaceClient.ts @@ -0,0 +1,78 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import core, { Client, Ref, TxOperations, type Blob } from '@hcengineering/core' +import drive, { createFile } from '@hcengineering/drive' +import love from '@hcengineering/love' +import { getClient } from './client' +import config from './config' +import { encode } from './jwt' + +export class WorkspaceClient { + private client!: TxOperations + + private constructor (private readonly workspace: string) {} + + static async create (workspace: string): Promise { + const instance = new WorkspaceClient(workspace) + await instance.initClient(workspace) + return instance + } + + async close (): Promise { + await this.client.close() + } + + private async initClient (workspace: string): Promise { + const token = encode({ + email: config.SystemEmail, + workspace + }) + const client = await getClient(token) + this.client = new TxOperations(client, core.account.System) + return this.client + } + + async saveFile (uuid: string, name: string): Promise { + const current = await this.client.findOne(drive.class.Drive, { _id: love.space.Drive }) + if (current === undefined) { + await this.client.createDoc( + drive.class.Drive, + core.space.Space, + { + private: false, + archived: false, + members: [], + name: 'Records', + description: 'Office records', + type: drive.spaceType.DefaultDrive, + autoJoin: true + }, + love.space.Drive + ) + } + + const blob = await this.client.findOne(core.class.Blob, { _id: uuid as Ref }) + if (blob !== undefined) { + const data = { + file: uuid as Ref, + name, + size: blob.size, + type: blob.contentType, + lastModified: blob.modifiedOn + } + await createFile(this.client, love.space.Drive, drive.ids.Root, data) + } + } +} diff --git a/services/love/tsconfig.json b/services/love/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/love/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/print/pod-print/.eslintrc.js b/services/print/pod-print/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/print/pod-print/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/print/pod-print/.npmignore b/services/print/pod-print/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/print/pod-print/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/print/pod-print/Dockerfile b/services/print/pod-print/Dockerfile new file mode 100644 index 0000000000..f2841ccf1c --- /dev/null +++ b/services/print/pod-print/Dockerfile @@ -0,0 +1,23 @@ + +FROM node:20 + +# We don't need the standalone Chromium +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true +# Set executable path for puppeteer +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable + +# Install Google Chrome Stable and fonts +# Note: this installs the necessary libs to make the browser work with Puppeteer. +RUN apt-get update && apt-get install gnupg wget -y dumb-init && \ + wget --quiet --output-document=- https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor > /etc/apt/trusted.gpg.d/google-archive.gpg && \ + sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' && \ + apt-get update && \ + apt-get install google-chrome-stable -y --no-install-recommends && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/src/app + +COPY bundle/bundle.js ./ +RUN npm install --ignore-scripts=false --verbose bufferutil utf-8-validate @mongodb-js/zstd snappy --unsafe-perm + +CMD [ "dumb-init", "node", "bundle.js" ] diff --git a/services/print/pod-print/config/rig.json b/services/print/pod-print/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/print/pod-print/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/print/pod-print/jest.config.js b/services/print/pod-print/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/print/pod-print/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/print/pod-print/package.json b/services/print/pod-print/package.json new file mode 100644 index 0000000000..e1a9904288 --- /dev/null +++ b/services/print/pod-print/package.json @@ -0,0 +1,66 @@ +{ + "name": "@hcengineering/pod-print", + "version": "0.1.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "_phase:bundle": "rushx bundle", + "_phase:docker-build": "rushx docker:build", + "_phase:docker-staging": "rushx docker:staging", + "bundle": "mkdir -p bundle && esbuild src/index.ts --bundle --platform=node --keep-names > bundle/bundle.js", + "docker:build": "cross-env DOCKER_EXTRA=--platform=linux/amd64 ../../../common/scripts/docker_build.sh hardcoreeng/print", + "docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/print staging", + "docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/print", + "run-local": "cross-env ts-node src/index.ts", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@tsconfig/node16": "^1.0.4", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/node": "~20.11.16", + "@types/ws": "^8.5.11", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "esbuild": "^0.20.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "prettier": "^3.1.0", + "ts-node": "^10.8.0", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/server-token": "^0.6.11", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/server-storage": "^0.6.0", + "@hcengineering/core": "^0.6.32", + "@hcengineering/platform": "^0.6.11", + "cors": "^2.8.5", + "dotenv": "~16.0.0", + "express": "^4.19.2", + "puppeteer": "^22.6.1", + "mammoth": "^1.6.0" + } +} diff --git a/services/print/pod-print/src/config.ts b/services/print/pod-print/src/config.ts new file mode 100644 index 0000000000..b7a12dd407 --- /dev/null +++ b/services/print/pod-print/src/config.ts @@ -0,0 +1,29 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +export interface Config { + Port: number + DbURL: string + Secret: string +} + +const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined) + +const config: Config = (() => { + const params: Partial = { + Port: parseNumber(process.env.PORT) ?? 4005, + DbURL: process.env.MONGO_URL, + Secret: process.env.SECRET + } + + const missingEnv = (Object.keys(params) as Array).filter((key) => params[key] === undefined) + + if (missingEnv.length > 0) { + throw Error(`Missing config for attributes: ${missingEnv.join(', ')}`) + } + + return params as Config +})() + +export default config diff --git a/services/print/pod-print/src/convert.ts b/services/print/pod-print/src/convert.ts new file mode 100644 index 0000000000..e10fc35027 --- /dev/null +++ b/services/print/pod-print/src/convert.ts @@ -0,0 +1,8 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +import mammoth from 'mammoth' + +export async function convertToHtml (buffer: Buffer): Promise { + return (await mammoth.convertToHtml({ buffer })).value +} diff --git a/services/print/pod-print/src/error.ts b/services/print/pod-print/src/error.ts new file mode 100644 index 0000000000..dc17f767c5 --- /dev/null +++ b/services/print/pod-print/src/error.ts @@ -0,0 +1,40 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +const generateMessage = (code: number): string => { + if (code === 401) { + return 'Unauthorized' + } + + if (code === 404) { + return 'Not Found' + } + + if (code === 400) { + return 'Bad Request' + } + + return 'Error' +} + +export class ApiError extends Error { + readonly code: number + + constructor (code: number, message?: string) { + super(message ?? generateMessage(code)) + this.code = code + } +} diff --git a/services/print/pod-print/src/index.ts b/services/print/pod-print/src/index.ts new file mode 100644 index 0000000000..e18b8ddb2a --- /dev/null +++ b/services/print/pod-print/src/index.ts @@ -0,0 +1,21 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { config } from 'dotenv' +import { main } from './main' + +config() +void main() diff --git a/services/print/pod-print/src/main.ts b/services/print/pod-print/src/main.ts new file mode 100644 index 0000000000..e7d90165f4 --- /dev/null +++ b/services/print/pod-print/src/main.ts @@ -0,0 +1,36 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import { setMetadata } from '@hcengineering/platform' +import serverToken from '@hcengineering/server-token' + +import { storageConfigFromEnv } from '@hcengineering/server-storage' +import config from './config' +import { createServer, listen } from './server' + +const setupMetadata = (): void => { + setMetadata(serverToken.metadata.Secret, config.Secret) +} + +export const main = async (): Promise => { + setupMetadata() + + const storageConfig = storageConfigFromEnv() + const { app, close } = createServer(config.DbURL, storageConfig) + const server = listen(app, config.Port) + + const shutdown = (): void => { + close() + server.close(() => process.exit()) + } + + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + process.on('uncaughtException', (e) => { + console.error(e) + }) + process.on('unhandledRejection', (e) => { + console.error(e) + }) +} diff --git a/services/print/pod-print/src/print.ts b/services/print/pod-print/src/print.ts new file mode 100644 index 0000000000..98eda58919 --- /dev/null +++ b/services/print/pod-print/src/print.ts @@ -0,0 +1,115 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +import puppeteer, { Page, Viewport } from 'puppeteer' + +export interface PrintOptions { + kind?: ExportKind + viewport?: Viewport +} + +export const validKinds = ['pdf', 'jpeg', 'png', 'webp'] as const + +export type ExportKind = (typeof validKinds)[number] + +/** + * Prints a webpage with the specified options + * @public + * @param url - The URL of the webpage to print. + * @param options - The options to use when printing the webpage. + * @returns Buffer with the printed content. + */ +export async function print (url: string, options?: PrintOptions): Promise { + const kind = options?.kind ?? 'pdf' + const viewport = options?.viewport ?? { width: 1440, height: 900 } + + // TODO: think of having a "hot" browser instance to avoid the overhead of launching a new one every time + const browser = await puppeteer.launch({ + headless: true, + args: [ + '--no-sandbox', + '--disable-gpu', + '--disable-dev-shm-usage', + '--disable-extensions', + '--disable-setuid-sandbox' + ] + }) + const page = await browser.newPage() + + await page.setViewport(viewport) + + // NOTE: Issues opened with a guest link worked fine only with networkidle0 here and + // waitForNetworkIdle 1000 afterwards. Also tried 700 but sometimes it was not enough. + await page.goto(url, { + waitUntil: ['domcontentloaded', 'networkidle0'] + }) + await page.waitForNetworkIdle({ idleTime: 1000 }) + + let res: Buffer | undefined + + if (kind === 'pdf') { + await page.emulateMediaType('print') + // Scroll throught the page to render all the content (e.g. as images are only rendered + // when they are visible in the viewport) + await scrollThrough(page) + + // Read page header and footer if defined + const pageHeader = await page.evaluate(() => { + const header = document.querySelector('#page-header') + return header !== null ? header.innerHTML : undefined + }) + + const pageFooter = await page.evaluate(() => { + const footer = document.querySelector('#page-footer') + return footer !== null ? footer.innerHTML : undefined + }) + + res = await page.pdf({ + format: 'A4', + landscape: false, + timeout: 0, + headerTemplate: pageHeader, + footerTemplate: pageFooter, + displayHeaderFooter: pageHeader !== undefined || pageFooter !== undefined, + margin: { + top: '1.5cm', + right: '1cm', + bottom: '1.5cm', + left: '1cm' + } + }) + } else { + // Note: currently we do not take the full page screenshot - only the viewport + // might make it configurable in the future + res = await page.screenshot({ type: kind }) + } + + await browser.close() + + return res +} + +async function scrollThrough (page: Page): Promise { + const MAX_SCROLLS = 10 + const TIMEOUT_BETWEEN_SCROLLS_MS = 400 + + await page.evaluate( + async (maxScrolls, timeoutBetweenScrollsMs) => { + let oldScrollY: number = 0 + let newScrollY: number = 0 + let count = 0 + + do { + oldScrollY = window.scrollY + window.scrollBy(0, window.innerHeight) + newScrollY = window.scrollY + // Wait for the page to render previously loaded images + // as they are only displayed when visible also content dependent on + // intersection observers etc... + await new Promise((resolve) => setTimeout(resolve, timeoutBetweenScrollsMs)) + } while (oldScrollY < newScrollY && count++ < maxScrolls) + }, + MAX_SCROLLS, + TIMEOUT_BETWEEN_SCROLLS_MS + ) +} diff --git a/services/print/pod-print/src/server.ts b/services/print/pod-print/src/server.ts new file mode 100644 index 0000000000..abff20f854 --- /dev/null +++ b/services/print/pod-print/src/server.ts @@ -0,0 +1,232 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { MeasureMetricsContext, generateId } from '@hcengineering/core' +import { StorageConfiguration } from '@hcengineering/server-core' +import { buildStorageFromConfig } from '@hcengineering/server-storage' +import { Token, decodeToken } from '@hcengineering/server-token' +import cors from 'cors' +import express, { type Express, type NextFunction, type Request, type Response } from 'express' +import { IncomingHttpHeaders, type Server } from 'http' + +import { convertToHtml } from './convert' +import { ApiError } from './error' +import { PrintOptions, print, validKinds } from './print' + +const extractCookieToken = (cookie?: string): Token | null => { + if (cookie === undefined || cookie === null) { + return null + } + + const cookies = cookie.split(';') + const tokenCookie = cookies.find((cookie) => cookie.toLocaleLowerCase().includes('token')) + if (tokenCookie === undefined) { + return null + } + + const encodedToken = tokenCookie.split('=')[1] + if (encodedToken === undefined) { + return null + } + + return decodeToken(encodedToken) +} + +const extractAuthorizationToken = (authorization?: string): Token | null => { + if (authorization === undefined || authorization === null) { + return null + } + const encodedToken = authorization.split(' ')[1] + + if (encodedToken === undefined) { + return null + } + + return decodeToken(encodedToken) +} + +const extractQueryToken = (queryParams: any): Token | null => { + if (queryParams == null) { + return null + } + + const encodedToken = queryParams.token + + if (encodedToken == null) { + return null + } + + return decodeToken(encodedToken) +} + +const extractToken = (headers: IncomingHttpHeaders, queryParams: any): Token => { + try { + const token = + extractCookieToken(headers.cookie) ?? + extractAuthorizationToken(headers.authorization) ?? + extractQueryToken(queryParams) + + if (token === null) { + throw new ApiError(401) + } + + return token + } catch { + throw new ApiError(401) + } +} + +type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise + +const handleRequest = async ( + fn: AsyncRequestHandler, + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const token = extractToken(req.headers, req.query) + await fn(req, res, token, next) + } catch (err: unknown) { + next(err) + } +} + +const wrapRequest = (fn: AsyncRequestHandler) => (req: Request, res: Response, next: NextFunction) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + handleRequest(fn, req, res, next) +} + +export function createServer (dbUrl: string, storageConfig: StorageConfiguration): { app: Express, close: () => void } { + const storageAdapter = buildStorageFromConfig(storageConfig, dbUrl) + const measureCtx = new MeasureMetricsContext('print', {}) + + const app = express() + app.use(cors()) + app.use(express.json()) + + app.get( + '/print', + wrapRequest(async (req, res, token) => { + const rawlink = req.query.link as string + const link = decodeURIComponent(rawlink) + + const kind = req.query.kind as PrintOptions['kind'] + + if (kind !== undefined && !validKinds.includes(kind as any)) { + throw new ApiError(400, `Invalid print kind: ${kind}`) + } + + const rawWidth = (req.query.width ?? '') as string + const rawHeight = (req.query.height ?? '') as string + + let viewport: PrintOptions['viewport'] | undefined + if (rawWidth.length > 0 && rawHeight.length > 0) { + viewport = { + width: parseInt(rawWidth, 10), + height: parseInt(rawHeight, 10) + } + + if (Number.isNaN(viewport.width) || Number.isNaN(viewport.height)) { + throw new ApiError(400, 'Invalid width or height') + } + } else if (rawWidth.length > 0 || rawHeight.length > 0) { + throw new ApiError(400, 'Both width and height must be provided') + } + + const printRes = await print(link, { kind, viewport }) + + if (printRes === undefined) { + throw new ApiError(400, 'Failed to print') + } + + const printId = `print-${generateId()}` + + await storageAdapter.put(measureCtx, token.workspace, printId, printRes, `application/${kind}`, printRes.length) + + res.contentType('application/json') + res.send({ id: printId }) + }) + ) + + app.get( + '/convert/:file', + wrapRequest(async (req, res, token) => { + const convertableFormats = ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'] + const file = req.params.file + const stat = await storageAdapter.stat(measureCtx, token.workspace, file) + + if (stat === undefined) { + throw new ApiError(404, `File ${file} not found`) + } + + if (!convertableFormats.includes(stat.contentType)) { + throw new ApiError(400, `File of this type (${stat.contentType}) cannot be converted`) + } + + const convertId = getConvertId(file, stat.etag) + const convertStats = await storageAdapter.stat(measureCtx, token.workspace, convertId) + + if (convertStats === undefined) { + const originalFile = await storageAdapter.read(measureCtx, token.workspace, file) + + if (originalFile === undefined) { + throw new ApiError(404, `File ${file} not found`) + } + + const htmlRes = await convertToHtml(Buffer.concat(originalFile)) + + if (htmlRes === undefined) { + throw new ApiError(400, 'Failed to convert') + } + + await storageAdapter.put(measureCtx, token.workspace, convertId, htmlRes, 'text/html', htmlRes.length) + } + + res.contentType('application/json') + res.send({ id: convertId }) + }) + ) + + app.use((err: any, _req: any, res: any, _next: any) => { + console.log(err) + if (err instanceof ApiError) { + res.status(err.code).send({ code: err.code, message: err.message }) + return + } + + res.status(500).send(err.message?.length > 0 ? { message: err.message } : err) + }) + + return { + app, + close: () => { + void storageAdapter.close() + } + } +} + +export function listen (e: Express, port: number, host?: string): Server { + const cb = (): void => { + console.log(`Print service has been started at ${host ?? '*'}:${port}`) + } + + return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb) +} + +function getConvertId (file: string, etag: string): string { + return `${file}@${etag}` +} diff --git a/services/print/pod-print/tsconfig.json b/services/print/pod-print/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/print/pod-print/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/rekoni/.eslintrc.js b/services/rekoni/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/rekoni/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/rekoni/Dockerfile b/services/rekoni/Dockerfile new file mode 100644 index 0000000000..607c028473 --- /dev/null +++ b/services/rekoni/Dockerfile @@ -0,0 +1,39 @@ +FROM node:20-alpine AS build + +WORKDIR /usr/src/app + +RUN apk upgrade --update && apk add \ + coreutils \ + libxml2-dev \ + html2text \ + autoconf \ + automake \ + build-base \ + gcc + +RUN cd ~ && \ + wget http://www.gnu.org/software/unrtf/unrtf-0.21.9.tar.gz && \ + tar xzvf unrtf-0.21.9.tar.gz && \ + cd unrtf-0.21.9/ && \ + ./bootstrap && \ + ./configure && \ + make && \ + make install + + +FROM node:20-alpine AS runtime + +RUN apk upgrade --update && apk add \ + coreutils \ + antiword \ + poppler-utils \ + html2text +ENV NODE_ENV=production +COPY --from=build /usr/local/bin/unrtf /usr/local/bin +RUN mkdir /usr/local/share/unrtf +COPY --from=build /usr/local/share/unrtf/* /usr/local/share/unrtf/ +WORKDIR /usr/src/app +RUN npm install --ignore-scripts=false --verbose sharp@v0.30.2 pdfjs-dist@v2.12.313 --unsafe-perm +COPY bundle/bundle.js ./ +EXPOSE 4004 +CMD [ "node", "./bundle.js" ] diff --git a/services/rekoni/config/rig.json b/services/rekoni/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/rekoni/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/rekoni/demo/hh_err1.pdf b/services/rekoni/demo/hh_err1.pdf new file mode 100644 index 0000000000..a0aa2686dd Binary files /dev/null and b/services/rekoni/demo/hh_err1.pdf differ diff --git a/services/rekoni/demo/pdf1.pdf b/services/rekoni/demo/pdf1.pdf new file mode 100644 index 0000000000..b429205a09 Binary files /dev/null and b/services/rekoni/demo/pdf1.pdf differ diff --git a/services/rekoni/demo/pdf1.txt b/services/rekoni/demo/pdf1.txt new file mode 100644 index 0000000000..51d845afbd --- /dev/null +++ b/services/rekoni/demo/pdf1.txt @@ -0,0 +1,418 @@ +10/21/21, 8:31 AM + +Sergey Ushnurtsev + +      + +  + + 2722 + +Inno.tech, QA Automation Engineer · + +Город: + +Москва + +Опыт работы + +8 лет и 5 месяцев + +Возраст: + +≈ 38 лет + +E-mail: + + oceanite@yandex.ru + + + +Sergey Ushnurtsev +Senior + +Соц.сети: + + https://github.com/sergggio + +Навыки: + +test automation +devops +junit + +Опыт работы: + +qa + +automation qa + +continuous integration +gitlab + +maven + +salesforce + +node.js + +databases + +tfs + +erp + +pip + +python + +test cases + +jenkins + +ux/ui + +testrail + +project management + +analysis + +kibana + +sql + +web design + +deployment + +logstash + +git + +elasticsearch + +bash +elk + + https://www.linkedin.com/in/sergey-ushnurtsev-33a989120 + +autocad + +crm + +jmeter + +soapui +ios + +java + +javascript +android + +babel + +selenium +groovy + +redmine + +bitbucket + +manual qa + +jira + +regression testing +cucumber + +mysql + +rest + +teamcity +json + +confluence + +soap + +selenide + +3d modeling + +mocha + +php + +английский + +Inno.tech + +Сентябрь 2020 - по настоящее + +QA Automation Engineer + +время +1 год и 2 месяца + +Иннотех + +Сентябрь 2020 - по настоящее + +QA Automation Engineer + +время +1 год и 2 месяца + +Current duties are: +Development of testing framework. Environment is: Java, Selenide, Maven, Cucumber, JUnit, RestAssured, Allure, Selenoid; +Developing and supporting UI and API tests; +Test documentation support, adding and updating test cases (TestIT, Confluence); +Continuous integration support: TeamCity; +Mocks: Soap UI, Mountebank. +ux/ui + +qa + +java + +selenide + +maven + +cucumber + +junit + +test cases + +confluence + +teamcity + +soapui + +soap + +Cashwagon + +Декабрь 2019 - Август 2020 + +QA Automation Engineer + +9 месяцев + +Current duties are: +Development of testing framework. Stack is: NodeJS, WebdriverIO v.5, GitLab, Jenkins, TestRail, BrowserStack, Selenoid, Slack alerts. +Develop and support for UI, E2E tests (website, CRM, API, PostgreDB). +TestRail integration for WebdriverIO, JMeter tests. +Test documentation support, adding and updating test cases (TestRail, Confluence). +Continuous integration support (Jenkins): pipelines. +Online courses passed: +https://www.udemy.com/course/automation-testing-javascript-webdriverio-selenium-more/ +selenium + +jenkins + +testrail + +qa + +node.js + +gitlab + +ux/ui + +crm + +jmeter + +test cases + +confluence + +BPC Banking Technologies + +Июль 2018 - Октябрь 2019 + +QA Test Engineer + +1 год и 4 месяца + +Current duties are: +Functional testing; +Work with bug-tracking system JIRA: creating and re-testing defects; +Test environment deployment; +Test documentation support, adding and updating test cases (Zephyr, Confluence); +Continuous integration duties (Jenkins): jobs and multijobs configuration, bash scripts coding; +Regression automation tests support: defects localization, logs analysis, creating JIRA tickets, updating tests due to version compatibility; +Creating new regression automation test requests using JMeter and SOAP UI. +Work with ELK (ElasticSearch+Logstash+Kibana): installing, updating, configuring. Creating Kibana's visualizations and dashboards; +Perfomance testing using JMeter: environment preparation, analysis of results and reports generation; +Python's scripts coding for generating test data; +Work with GitLab repository; +ISO 8583 protocol and messages testing; +Integration test interaction with other company's systems. +Achievements: +Implementation support at customer in Indonesia (march - may 2019): invironment preparation and configuration, Skype-trainings and business trip to Jakarta. +https://app.podbor.io/search-profile.php?id=c1c4116d186a02a0587f09057df79b97 + +1/2 + + 10/21/21, 8:31 AM + +Sergey Ushnurtsev + +qa + +bash + +kibana + +elasticsearch + +analysis + +logstash + +jmeter + +jira + +confluence + +jenkins + +test cases + +deployment + +bug tracking + +soapui + +soap + +ux/ui + +elk + +gitlab + +Утконос + +Июль 2017 - Июль 2018 + +QA Tester + +1 год и 1 месяц + +Forming testing process at developer team. I worked as a single test engineer in team. +My duties were: +Functional, integration, regression testing of web-site utkonos.ru (front-end + back-end); +API testing for mobile version site, Android and iOS apps; +Functional, exploratory and regression testing of mobile version, Android and iOS apps; +Work with incoming tickets from IT Support team; +Work with bug-tracking system JIRA (creating, solving, testing, closing defects); +Writing internal documentation for company departments: instructions, API-method descriptions; +Work with MySQL database (SQL requests for testing); +Jenkins administration and configuration. +Achievements: +CI implementation (Jenkins); +Automation tests implementation (API tests - REST-assured, UI tests - Selenium/Selenide + Java + Maven + JUnit + Bitbucket + Jenkins). +qa +junit + +regression testing + +jenkins + +ios + +android + +bug tracking + +jira + +mysql + +sql + +rest + +ux/ui + +selenium + +selenide + +java + +maven + +bitbucket + +Moscow City Telephone Network (MGTS) + +Сентябрь 2016 - Июль 2017 + +QA Tester + +11 месяцев + +Functional, integration and regression testing of company products: CRM, billing, address systems; +Work in bug-tracking systems (TFS, Redmine); +Adding new test cases, updating existing test cases based on requirements and documentation; +Creating SQL requests for regression test cases. +test cases + +redmine + +regression testing + +crm + +bug tracking + +tfs + +sql + +Тион + +Январь 2013 - Август 2016 + +Technical Support Engineering + +3 года и 8 месяцев + +CRM administrator: Salesforce CRM, AVA ERP; +Autocad 2D/3D modeling: design of ventilation and air cleaning systems. +crm + +Образование: + +salesforce + +3d modeling + +erp + +autocad + +Национальный Исследовательский Технологический Университет - «МИСиС» (бывший Московский + +2000 - 2005 + +Государственный Институт Стали и Сплавов) +Диплом специалиста по инженерной специальности + +https://app.podbor.io/search-profile.php?id=c1c4116d186a02a0587f09057df79b97 + +2/2 + + \ No newline at end of file diff --git a/services/rekoni/demo/pdf2.pdf b/services/rekoni/demo/pdf2.pdf new file mode 100644 index 0000000000..c602e2ebf2 Binary files /dev/null and b/services/rekoni/demo/pdf2.pdf differ diff --git a/services/rekoni/demo/pdf3.pdf b/services/rekoni/demo/pdf3.pdf new file mode 100644 index 0000000000..afbcecb58c Binary files /dev/null and b/services/rekoni/demo/pdf3.pdf differ diff --git a/services/rekoni/demo/pdf4.pdf b/services/rekoni/demo/pdf4.pdf new file mode 100644 index 0000000000..5945c5508d Binary files /dev/null and b/services/rekoni/demo/pdf4.pdf differ diff --git a/services/rekoni/demo/pdf5.pdf b/services/rekoni/demo/pdf5.pdf new file mode 100644 index 0000000000..460e978d45 Binary files /dev/null and b/services/rekoni/demo/pdf5.pdf differ diff --git a/services/rekoni/demo/pdf6.pdf b/services/rekoni/demo/pdf6.pdf new file mode 100644 index 0000000000..9f714f2688 Binary files /dev/null and b/services/rekoni/demo/pdf6.pdf differ diff --git a/services/rekoni/demo/Резюме Власова Анатолия.docx b/services/rekoni/demo/Резюме Власова Анатолия.docx new file mode 100644 index 0000000000..4b326399ce Binary files /dev/null and b/services/rekoni/demo/Резюме Власова Анатолия.docx differ diff --git a/services/rekoni/docker_tag.sh b/services/rekoni/docker_tag.sh new file mode 100755 index 0000000000..ec0574be47 --- /dev/null +++ b/services/rekoni/docker_tag.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +version=$(git describe --tags --abbrev=0) + +if [ "x$2" = "xstaging" ] +then + a=( ${version//./ } ) + ((a[2]++)) + version="${a[0]}.${a[1]}.${a[2]}-staging" + echo "Tagging stating $1 with version ${version}" + docker tag "$1" "$1:$version" + docker push "$1:$version" +else + echo "Tagging release $1 with version ${version}" + docker tag "$1" "$1:$version" + docker push "$1:$version" + docker push "$1:latest" +fi diff --git a/services/rekoni/esbuild.js b/services/rekoni/esbuild.js new file mode 100644 index 0000000000..4d1ea8fa09 --- /dev/null +++ b/services/rekoni/esbuild.js @@ -0,0 +1,12 @@ +const esbuild = require('esbuild') + +void esbuild.build({ + entryPoints: ['src/index.ts'], + bundle: true, + minify: true, + keepNames: true, + loader: { ".node": "file" }, + platform: 'node', + outfile: 'bundle/bundle.js', + external: ['canvas', 'sharp', 'pdfjs-dist'] +}) \ No newline at end of file diff --git a/services/rekoni/jest.config.js b/services/rekoni/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/rekoni/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/rekoni/package.json b/services/rekoni/package.json new file mode 100644 index 0000000000..a842386a7f --- /dev/null +++ b/services/rekoni/package.json @@ -0,0 +1,86 @@ +{ + "name": "@hcengineering/rekoni-service", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "UNLICENSED", + "scripts": { + "build": "compile", + "_phase:bundle": "rushx bundle", + "_phase:docker-build": "rushx docker:build", + "_phase:docker-staging": "rushx docker:staging", + "docker:build": "docker build -t hardcoreeng/rekoni-service .", + "docker:staging": "./docker_tag.sh hardcoreeng/rekoni-service staging", + "docker:push": "./docker_tag.sh hardcoreeng/rekoni-service", + "bundle": "node esbuild.js", + "start": "ts-node-dev src/index.ts", + "run-local": "cross-env ts-node src/index.ts", + "check-ts": "tsc --noEmit", + "lint": "eslint src", + "lint:fix": "eslint --fix src", + "test": "jest --passWithNoTests --silent", + "format": "format src", + "find-skills": "ts-node ./src/find-skills.ts", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate", + "build:watch": "compile" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/html-to-text": "^8.1.1", + "@types/jimp": "^0.2.28", + "@types/node": "~20.11.16", + "@types/pdfjs-dist": "2.10.378", + "@types/sharp": "~0.32.0", + "node-loader": "~2.0.0", + "@types/body-parser": "~1.19.2", + "@types/mime-types": "~2.1.1", + "@types/express-form-data": "^2.0.5", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "ts-node": "^10.8.0", + "ts-node-dev": "^2.0.0", + "typescript": "^5.3.3", + "esbuild": "^0.20.0", + "eslint-plugin-n": "^15.4.0", + "prettier": "^3.1.0" + }, + "dependencies": { + "@anticrm/skillset": "^0.6.0", + "@types/email-addresses": "^3.0.0", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "dotenv": "~16.0.0", + "email-addresses": "^5.0.0", + "express": "^4.19.2", + "express-form-data": "^2.0.22", + "html-to-text": "^9.0.3", + "jimp": "^0.16.1", + "jwt-simple": "^0.5.6", + "libphonenumber-js": "^1.9.46", + "mammoth": "^1.6.0", + "mime-types": "~2.1.34", + "pdfjs-dist": "2.12.313", + "sharp": "~0.32.0" + }, + "description": "Document recognition service" +} diff --git a/services/rekoni/src/__tests__/demo.test.ts b/services/rekoni/src/__tests__/demo.test.ts new file mode 100644 index 0000000000..d48bb704a7 --- /dev/null +++ b/services/rekoni/src/__tests__/demo.test.ts @@ -0,0 +1,84 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import { readFile } from 'fs/promises' +import { extractDocument } from '../process' + +describe('pdf-parse', () => { + it('check hh6', async () => { + const data = await readFile('./demo/pdf6.pdf') + const { resume } = await extractDocument(data) + + expect(resume.firstName).toBe('Виктория') + expect(resume.lastName).toBe('Бондаренко') + expect(resume.email).toBe('wika.schneider@mail.ru') + expect(resume.phone).toBe('+7 952 929 53 49') + }) + it('check hh5', async () => { + const data = await readFile('./demo/pdf5.pdf') + const { resume } = await extractDocument(data) + + expect(resume.firstName).toBe('Александр') + expect(resume.lastName).toBe('Сизых') + expect(resume.city).toBe('Томск') + expect(resume.email).toBe('alexsneezy@gmail.com') + expect(resume.phone).toBe('+7 953 920 11 22') + expect(resume.skype).toBe('https://join.skype.com/invite/hDFFG9b7tvRb') + expect(resume.linkedin).toBe('http://linkedin.com/in/asizykh') + }) + it('check hh4', async () => { + const data = await readFile('./demo/pdf4.pdf') + const { resume } = await extractDocument(data) + + expect(resume.firstName).toBe('Алексей Владимирович') + expect(resume.lastName).toBe('Пешков') + expect(resume.city).toBe('Москва, м. Тимирязевская') + expect(resume.email).toBe('aleks.peshkov@gmail.com') + }) + + it('check hh3', async () => { + const data = await readFile('./demo/pdf3.pdf') + const { resume } = await extractDocument(data) + + expect(resume.firstName).toBe('Ильнур') + expect(resume.lastName).toBe('Сербаев') + expect(resume.phone).toBe('+7 917 049 18 13') + expect(resume.skype).toBe('serbaevilnur') + expect(resume.city).toBe('Уфа') + expect(resume.email).toBe('serbaevilnur@gmail.com') + }) + + it('check hh2', async () => { + const data = await readFile('./demo/pdf2.pdf') + const { resume } = await extractDocument(data) + + expect(resume.firstName).toBe('Дмитрий') + expect(resume.lastName).toBe('Войтенко') + expect(resume.phone).toBe('+7 960 986 12 95') + expect(resume.email).toBe('13unconnected37@gmail.com') + expect(resume.city).toBe('Новосибирск, м. Золотая нива') + }) + + it('check podbor', async () => { + const data = await readFile('./demo/pdf1.pdf') + const { resume } = await extractDocument(data) + + expect(resume.firstName).toBe('Sergey') + expect(resume.lastName).toBe('Ushnurtsev') + expect(resume.email).toBe('oceanite@yandex.ru') + expect(resume.city).toBe('Москва') + expect(resume.github).toBe('https://github.com/sergggio') + }) +}) diff --git a/services/rekoni/src/__tests__/extractor.test_.ts b/services/rekoni/src/__tests__/extractor.test_.ts new file mode 100644 index 0000000000..578e4e50a6 --- /dev/null +++ b/services/rekoni/src/__tests__/extractor.test_.ts @@ -0,0 +1,43 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import { readFile } from 'fs/promises' +import { lookup } from 'mime-types' +import { extract } from '../extractors' + +describe('extractor-tests', () => { + it('check docx', async () => { + const fName = './demo/Резюме Власова Анатолия.docx' + const data = await readFile(fName) + const type = lookup(fName) + + const { matched, content } = await extract(fName, type, data) + + expect(matched).toBe(true) + expect(content).toContain('садовник') + expect(content).toContain('Власов Анатолий') + }) + it('check pdf', async () => { + const fName = './demo/pdf1.pdf' + const data = await readFile(fName) + const type = lookup(fName) + + const { matched, content } = await extract(fName, type, data) + + expect(matched).toBe(true) + expect(content).toContain('kibana') + expect(content).toContain('Automation tests') + }) +}) diff --git a/services/rekoni/src/__tests__/hh.test.ts b/services/rekoni/src/__tests__/hh.test.ts new file mode 100644 index 0000000000..accd3648cf --- /dev/null +++ b/services/rekoni/src/__tests__/hh.test.ts @@ -0,0 +1,31 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import { readFile } from 'fs/promises' +import { extractDocument } from '../process' + +describe('pdf-parse', () => { + it('check hh6', async () => { + const data = await readFile('./demo/hh_err1.pdf') + const { resume } = await extractDocument(data) + + expect(resume.format).toBe('unknown') + expect(resume.firstName).toBe('Prosolupov') + expect(resume.lastName).toBe('Dmitry') + expect(resume.email).toBe('dmpros1987@gmail.com') + expect(resume.phone).toBe('+7 919 117 62 97') + expect(resume.avatarName).toBe('img_p0_1.jpeg') + }) +}) diff --git a/services/rekoni/src/config.ts b/services/rekoni/src/config.ts new file mode 100644 index 0000000000..580182e1e7 --- /dev/null +++ b/services/rekoni/src/config.ts @@ -0,0 +1,50 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +interface Config { + Port: number + + ServiceID: string + Secret: string +} + +const envMap: { [key in keyof Config]: string } = { + Port: 'PORT', + + ServiceID: 'SERVICE_ID', + Secret: 'SECRET' +} + +const required: Array = ['ServiceID', 'Secret'] + +const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined) + +const config: Config = (() => { + const params: Partial = { + Port: parseNumber(process.env[envMap.Port] ?? '4004'), + ServiceID: process.env[envMap.ServiceID] ?? 'rekoni-service', + Secret: process.env[envMap.Secret] ?? 'secret' + } + + const missingEnv = required.filter((key) => params[key] === undefined).map((key) => envMap[key]) + + if (missingEnv.length > 0) { + throw Error(`Missing env variables: ${missingEnv.join(', ')}`) + } + + return params as Config +})() + +export default config diff --git a/services/rekoni/src/error.ts b/services/rekoni/src/error.ts new file mode 100644 index 0000000000..998225373f --- /dev/null +++ b/services/rekoni/src/error.ts @@ -0,0 +1,23 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export class ApiError extends Error { + constructor ( + readonly code: string, + readonly message: string + ) { + super(message) + } +} diff --git a/services/rekoni/src/extractors/doc.ts b/services/rekoni/src/extractors/doc.ts new file mode 100644 index 0000000000..1df888886d --- /dev/null +++ b/services/rekoni/src/extractors/doc.ts @@ -0,0 +1,55 @@ +import { exec } from 'child_process' +import { mkdtemp, rm, rmdir, writeFile } from 'fs/promises' +import { contentType } from 'mime-types' +import { tmpdir } from 'os' +import { join } from 'path' +import { rtfExtractor } from './rtf' +import { DocumentExtractor } from './types' + +export const docExtractor: DocumentExtractor = { + async isMatch (fileName: string, type: string | false, data): Promise { + if (type === false) return false + if (isType(type)) { + return true + } + // Try detect by fileName + type = contentType(fileName) + return type === false ? false : isType(type) + }, + + async extract (fileName: string, type: string, data): Promise { + const tempDir = await mkdtemp(join(tmpdir(), 'rekoni-')) + const distFileName = join(tempDir, 'content.doc') + await writeFile(distFileName, data) + const text = await new Promise((resolve, reject) => { + exec( + `antiword -i 1 -f -m UTF-8 "${distFileName}"`, + { encoding: 'utf-8', cwd: tempDir }, + (error, stdout, stderr) => { + if (error != null) { + if (stderr.includes('is not a Word Document. It is probably a Rich Text Format file')) { + rtfExtractor + .extract(fileName, type, data) + .then((value) => { + resolve(value) + }) + .catch((err) => { + reject(err) + }) + return + } + reject(new Error(`Error ${JSON.stringify(error)} ${stderr}`)) + } else { + resolve(stdout) + } + } + ) + }) + await rm(distFileName) + await rmdir(tempDir) + return text + } +} +function isType (type: string): boolean { + return type === 'application/msword' +} diff --git a/services/rekoni/src/extractors/docx.ts b/services/rekoni/src/extractors/docx.ts new file mode 100644 index 0000000000..4b41651b84 --- /dev/null +++ b/services/rekoni/src/extractors/docx.ts @@ -0,0 +1,38 @@ +import { convertToHtml, images } from 'mammoth' +import { contentType } from 'mime-types' +import { DocumentExtractor } from './types' + +import { convertString } from './html' + +export const docxExtractor: DocumentExtractor = { + async isMatch (fileName: string, type: string | false, data): Promise { + if (type === false) return false + if (isType(type)) { + return true + } + // Try detect by fileName + type = contentType(fileName) + return type === false ? false : isType(type) + }, + + async extract (fileName: string, type: string, data): Promise { + const htmlData = await convertToHtml( + { buffer: data }, + { + convertImage: images.imgElement((image) => { + return image.read('base64').then(function (imageBuffer) { + return { + src: '' + } + }) + }) + } + ) + + const text = convertString(htmlData.value) + return text + } +} +function isType (type: string): boolean { + return type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' +} diff --git a/services/rekoni/src/extractors/html.ts b/services/rekoni/src/extractors/html.ts new file mode 100644 index 0000000000..748cc43739 --- /dev/null +++ b/services/rekoni/src/extractors/html.ts @@ -0,0 +1,28 @@ +import { convert } from 'html-to-text' +import { contentType } from 'mime-types' +import { DocumentExtractor } from './types' + +export const htmlExtractor: DocumentExtractor = { + async isMatch (fileName: string, type: string | false, data): Promise { + if (type === false) return false + if (isType(type)) { + return true + } + // Try detect by fileName + type = contentType(fileName) + return type === false ? false : isType(type) + }, + + async extract (fileName: string, type: string, data): Promise { + return convertString(data.toString()) + } +} +export function convertString (text: string): string { + return convert(text, { + preserveNewlines: true, + selectors: [{ selector: 'img', format: 'skip' }] + }) +} +function isType (type: string): boolean { + return type === 'text/html' || type === 'application/xhtml+xml' +} diff --git a/services/rekoni/src/extractors/index.ts b/services/rekoni/src/extractors/index.ts new file mode 100644 index 0000000000..f6ee170a1e --- /dev/null +++ b/services/rekoni/src/extractors/index.ts @@ -0,0 +1,39 @@ +import {} from 'mime-types' +import { docExtractor } from './doc' +import { docxExtractor } from './docx' +import { htmlExtractor } from './html' +import { pdfExtractor } from './pdf' +import { rtfExtractor } from './rtf' +import { DocumentExtractor } from './types' +export * from './types' + +const extractors: Record = { + pdf: pdfExtractor, + rtf: rtfExtractor, + html: htmlExtractor, + doc: docExtractor, + docx: docxExtractor +} + +export async function extract ( + fileName: string, + type: string | false, + data: Buffer +): Promise<{ matched: boolean, content: string, error?: any }> { + if (type === false) { + return { matched: true, content: '', error: new Error('Unknown file type') } + } + console.log('extracting text ', fileName, type) + for (const ex of Object.entries(extractors)) { + if (await ex[1].isMatch(fileName, type, data)) { + try { + const content = await ex[1].extract(fileName, type, data) + return { matched: true, content } + } catch (err: any) { + console.log(err) + return { matched: true, content: '', error: err } + } + } + } + return { matched: false, content: '' } +} diff --git a/services/rekoni/src/extractors/pdf.ts b/services/rekoni/src/extractors/pdf.ts new file mode 100644 index 0000000000..ffd55c6736 --- /dev/null +++ b/services/rekoni/src/extractors/pdf.ts @@ -0,0 +1,42 @@ +import { exec } from 'child_process' +import { mkdtemp, rm, rmdir, writeFile } from 'fs/promises' +import { contentType } from 'mime-types' +import { tmpdir } from 'os' +import { join } from 'path' +import { DocumentExtractor } from './types' + +export const pdfExtractor: DocumentExtractor = { + async isMatch (fileName: string, type: string | false, data): Promise { + if (type === false) return false + if (type.includes('application/pdf')) { + return true + } + // Try detect by fileName + type = contentType(fileName) + if (type === 'application/pdf') { + return true + } + + // TODO: Try detect from buffer + return false + }, + + async extract (fileName: string, type: string, data): Promise { + const tempDir = await mkdtemp(join(tmpdir(), 'rekoni-')) + const distFileName = join(tempDir, 'content.pdf') + await writeFile(distFileName, data) + + const text = await new Promise((resolve, reject) => { + exec(`pdftotext -layout "${distFileName}" -`, { encoding: 'utf-8', cwd: tempDir }, (error, stdout, stderr) => { + if (error != null) { + reject(new Error(`Error ${JSON.stringify(error)} ${stderr}`)) + } else { + resolve(stdout) + } + }) + }) + await rm(distFileName) + await rmdir(tempDir) + return text + } +} diff --git a/services/rekoni/src/extractors/rtf.ts b/services/rekoni/src/extractors/rtf.ts new file mode 100644 index 0000000000..896d52fb77 --- /dev/null +++ b/services/rekoni/src/extractors/rtf.ts @@ -0,0 +1,42 @@ +import { exec } from 'child_process' +import { mkdtemp, rm, rmdir, writeFile } from 'fs/promises' +import { contentType } from 'mime-types' +import { tmpdir } from 'os' +import { join } from 'path' +import { convertString } from './html' +import { DocumentExtractor } from './types' + +export const rtfExtractor: DocumentExtractor = { + async isMatch (fileName: string, type: string | false, data): Promise { + if (type === false) return false + if (isType(type)) { + return true + } + // Try detect by fileName + type = contentType(fileName) + return type === false ? false : isType(type) + }, + + async extract (fileName: string, type: string, data): Promise { + const tempDir = await mkdtemp(join(tmpdir(), 'rekoni-')) + const distFileName = join(tempDir, 'content.rtf') + await writeFile(distFileName, data) + const htmlText = await new Promise((resolve, reject) => { + exec(`unrtf --nopict --html "${distFileName}"`, { encoding: 'utf-8', cwd: tempDir }, (error, stdout, stderr) => { + if (error != null) { + reject(new Error(`Error ${JSON.stringify(error)} ${stderr}`)) + } else { + resolve(stdout) + } + }) + }) + + const text = convertString(htmlText) + await rm(distFileName) + await rmdir(tempDir) + return text + } +} +function isType (type: string): boolean { + return type === 'application/rtf' || type === 'text/rtf' +} diff --git a/services/rekoni/src/extractors/types.ts b/services/rekoni/src/extractors/types.ts new file mode 100644 index 0000000000..298142b83b --- /dev/null +++ b/services/rekoni/src/extractors/types.ts @@ -0,0 +1,4 @@ +export interface DocumentExtractor { + isMatch: (fileName: string, type: string, data: Buffer) => Promise + extract: (fileName: string, type: string, data: Buffer) => Promise +} diff --git a/services/rekoni/src/extractpdf.ts b/services/rekoni/src/extractpdf.ts new file mode 100644 index 0000000000..5ee052327c --- /dev/null +++ b/services/rekoni/src/extractpdf.ts @@ -0,0 +1,194 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { getDocument, OPS, type PDFPageProxy } from 'pdfjs-dist/legacy/build/pdf' +import { type TextItem, type TypedArray } from 'pdfjs-dist/types/src/display/api' +import sharp from 'sharp' +import { type RekoniModel } from './types' +import { isPrivateCharCode } from './utils' + +// Some PDFs need external cmaps. +const CMAP_URL = './node_modules/pdfjs-dist/cmaps/' +const CMAP_PACKED = true + +// Where the standard fonts are located. +const STANDARD_FONT_DATA_URL = './node_modules/pdfjs-dist/standard_fonts/' + +/** + * @public + */ +export async function extractData (data: string | TypedArray): Promise { + const doc = await getDocument({ + data, + cMapUrl: CMAP_URL, + cMapPacked: CMAP_PACKED, + standardFontDataUrl: STANDARD_FONT_DATA_URL + }).promise + const meta = await doc.getMetadata() + + const text: RekoniModel = { + lines: [], + annotations: [], + images: [], + author: (meta.info as any).Author + } + + const imageOps = [ + OPS.paintJpegXObject, + OPS.paintImageMaskXObject, + OPS.paintImageMaskXObjectGroup, + OPS.paintImageXObject, + OPS.paintInlineImageXObject, + OPS.paintInlineImageXObjectGroup, + OPS.paintImageXObjectRepeat, + OPS.paintImageMaskXObjectRepeat, + OPS.paintSolidColorImageMask + ] + + for (let i = 1; i <= doc.numPages; i++) { + const page = await doc.getPage(i) + + await processAnnotations(page, text) + await processImages(page, imageOps, text) + await processPage(text, page) + } + + return text +} + +async function processPage (text: RekoniModel, page: PDFPageProxy): Promise { + const textContent = await page.getTextContent({ + normalizeWhitespace: true, + disableCombineTextItems: false, + includeMarkedContent: false + }) + + let lastY: number = 0 + let ctext: string[] = [] + let widths: number[] = [] + let maxH = 0 + for (const item of textContent.items as TextItem[]) { + const str = item.str + if (str.length === 1) { + const code = str.charCodeAt(0) + if (isPrivateCharCode(code)) { + // Private use characrter, skip it + continue + } + } + if (lastY === item.transform[5] || lastY === 0) { + if (str.length > 0) { + ctext.push(item.str) + widths.push(item.width) + } + if (item.height > maxH) { + maxH = item.height + } + } else { + text.lines.push({ items: ctext, height: maxH, widths }) + maxH = item.height + if (str.length > 0) { + ctext = [str] + widths = [item.width] + } else { + ctext = [] + widths = [] + } + } + + lastY = item.transform[5] + } + text.lines.push({ items: ctext, height: maxH, widths }) +} + +async function processAnnotations (page: PDFPageProxy, text: RekoniModel): Promise { + const annotations = await page.getAnnotations({ intent: 'any' }) + for (const ann of annotations) { + if ((ann.subtype ?? '').toLowerCase() === 'link' && ann.url !== undefined) { + text.annotations.push({ type: ann.subtype.toLowerCase(), value: ann.url }) + } + } +} + +async function processImages (page: PDFPageProxy, imageOps: number[], text: RekoniModel): Promise { + const operators = await page.getOperatorList() + for (let i = 0; i < operators.fnArray.length; i++) { + const tt = operators.fnArray[i] + if (imageOps.includes(tt)) { + const obj = operators.argsArray[i][0] as string + let objData: any + if (obj.startsWith('g_')) { + objData = page.commonObjs.get(obj) + } else { + objData = page.objs.get(obj) + } + if (objData !== undefined) { + const width = objData.width + const height = objData.height + + const rawBuffer = processImageData(objData) + + const img = sharp(rawBuffer, { + raw: { + width, + height, + channels: 4 + } + }) + + const pngBuffer = await img.toFormat('png').toBuffer() + const buffer = await img.toFormat('jpeg').toBuffer() + + text.images.push({ + name: obj + '.jpeg', + width, + height, + buffer, + pngBuffer, + format: 'image/jpeg', + potentialAvatar: true + }) + } + } + } +} + +function processImageData (objData: any): Buffer { + const result = Buffer.alloc(objData.width * objData.height * 4) + if (objData.kind === 2) { + // ImageKind.RGB_24BPP) { + let pos = 0 + for (let i = 0; i < objData.data.length; i += 3) { + result[pos] = objData.data[i] + result[pos + 1] = objData.data[i + 1] + result[pos + 2] = objData.data[i + 2] + result[pos + 3] = 0xff + + pos += 4 + } + } else if (objData.kind === 3) { + // Util.ImageKind.RGBA_32BPP) { + let pos = 0 + for (let i = 0; i < objData.data.length; i += 4) { + result[pos] = objData.data[i] + result[pos + 1] = objData.data[i + 1] + result[pos + 2] = objData.data[i + 2] + result[pos + 3] = objData.data[i + 3] + + pos += 4 + } + } + return result +} diff --git a/services/rekoni/src/find-skills.ts b/services/rekoni/src/find-skills.ts new file mode 100644 index 0000000000..ac82552210 --- /dev/null +++ b/services/rekoni/src/find-skills.ts @@ -0,0 +1,89 @@ +import { getCategories, SkillCategory } from '@anticrm/skillset' +import { existsSync, mkdirSync, readdirSync, readFileSync } from 'fs' +import { writeFile } from 'fs/promises' +import { join } from 'path' +import { extractDocument } from './process' + +const pdfDir = process.argv[2] + +const files = readdirSync(pdfDir) + +const skills = new Map() + +const formats = new Map() + +const resumesRoot = './resumes' +if (!existsSync(resumesRoot)) { + mkdirSync(resumesRoot) +} + +const categories = getCategories() + +async function readFiles (): Promise { + for (const filename of files) { + const pdfFile = join(pdfDir, filename) + const data = readFileSync(pdfFile) + + const { resume, model } = await extractDocument(data) + if (resume !== undefined) { + formats.set(resume.format, (formats.get(resume.format) ?? 0) + 1) + + console.log('resume for', resume.format, resume.firstName, resume.lastName) + if (resume.skills.length > 0) { + for (const s of resume.skills) { + // Check + const es = skills.get(s) ?? { count: 0, category: undefined } + es.count++ + if (es.category === undefined) { + // Try find category. + for (const c of categories) { + if (c.skills.findIndex((it) => it.toLowerCase() === s.toLowerCase()) !== -1) { + es.category = c + break + } + } + } + skills.set(s, es) + } + } + const path = [resumesRoot, resume.format] + if (resume.skills.length === 0) { + path.push('no-skills') + } else { + path.push('skills') + } + if (!existsSync(join(...path))) { + mkdirSync(join(...path), { recursive: true }) + } + await writeFile(join(...path, filename), data) + await writeFile(join(...path, filename + '.resume.json'), JSON.stringify(resume, undefined, 2)) + for (const img of model.images) { + await writeFile(join(...path, filename + '._' + img.name + '.png'), img.pngBuffer) + } + model.images = [] + await writeFile(join(...path, filename + '.model.json'), JSON.stringify(model, undefined, 2)) + } + } + const filteredSkills = Array.from(skills.entries()) + .filter((it) => it[1].count > 1) + .map((it) => it[0]) + + const raw = Array.from(skills.entries()) + raw.sort((a, b) => b[1].count - a[1].count) + + await writeFile('./skills.json', JSON.stringify(filteredSkills, undefined, 2)) + await writeFile( + './skills_raw.json', + JSON.stringify( + raw.map((it) => `${it[0]}-${it[1].count}-${(it[1].category?.label as string) ?? 'Unknown'}`), + undefined, + 2 + ) + ) + + console.log('Formats', Array.from(formats.entries())) +} + +readFiles().catch((err) => { + console.error(err) +}) diff --git a/services/rekoni/src/generic.ts b/services/rekoni/src/generic.ts new file mode 100644 index 0000000000..7ecbf67dd9 --- /dev/null +++ b/services/rekoni/src/generic.ts @@ -0,0 +1,84 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import addrs from 'email-addresses' +import parsePhoneNumber from 'libphonenumber-js' +import { lowePossibleSkills, parseSkillsRaw } from './skills' +import { ReconiDocument, RekoniModel } from './types' +import { getLineItems, handleSkills } from './utils' + +export function parseGenericResume (text: RekoniModel, resume: ReconiDocument): void { + const sorted = [...text.lines] + sorted.sort((a, b) => b.height - a.height) + + const title = sorted.shift() + if (title !== undefined) { + const titems = getLineItems(title.items, true).filter((t) => t.length > 0) + if (titems.length >= 2) { + resume.lastName = titems[0] + resume.firstName = titems.slice(1).join(' ') + } else if (titems.length === 1) { + const ti = titems[0].split(' ') + if (ti.length >= 2) { + resume.lastName = ti[0] + resume.firstName = ti.slice(1).join(' ') + } + } + } + let skillsHeight = 0 + let skills = false + for (const t of text.lines) { + const line = getLineItems(t.items, false).join('').trim() + if (resume.phone === undefined) { + const phone = parsePhoneNumber(line) + if (phone !== undefined) { + resume.phone = phone.formatInternational() + } + } + + if (resume.email === undefined) { + for (const ll of line.split(' ')) { + const parsedddr = addrs.parseOneAddress(ll) + if (parsedddr != null) { + resume.email = ll + break + } + } + } + + if (t.height !== skillsHeight) { + if (skillsHeight === -1) { + skillsHeight = t.height + } else { + skills = false + } + } + if (t.items.length > 0) { + const first = t.items[0].toLowerCase() + if (first === 'навыки' || first === 'skills' || first === 'skills & expertise') { + skills = true + skillsHeight = t.height + handleSkills(t.items.slice(1), resume, (ss) => lowePossibleSkills.has(ss.toLowerCase())) + if (resume.skills.length === 0) { + // No skills on this line, skipping it. + skillsHeight = -1 + } + } else if (skills) { + handleSkills(t.items, resume, (ss) => lowePossibleSkills.has(ss.toLowerCase())) + } + } + } + parseSkillsRaw(resume, text) +} diff --git a/services/rekoni/src/headhunter.ts b/services/rekoni/src/headhunter.ts new file mode 100644 index 0000000000..3fbc4d0a78 --- /dev/null +++ b/services/rekoni/src/headhunter.ts @@ -0,0 +1,152 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import addrs from 'email-addresses' +import Jimp from 'jimp' +import parsePhoneNumber from 'libphonenumber-js' +import { parseSkillsRaw } from './skills' +import { type ReconiDocument, type RekoniModel } from './types' +import { getLineItems, handleSkills } from './utils' + +export async function isHeadHunter (model: RekoniModel): Promise { + const hh1 = + 'iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAAAklEQVR4AewaftIAAAeDSURBVO3BfWzU9R3A8ffne3e0FNq7ChPaHmsrVco1YtlfI5kb+ocUFcWomLE/IIzFpRAUNct0D11JJMsyrSzSLHEguoSkIS5IjIMsQzrdaDIiLVuv5Wlc17tCWbXXHpytP/v7rOqR1Esfru1dse29XsIUa6agXGAFUAJSougSYAFIrsBiBilcAe0GPhKkHfQCcEHhTBmhRqaQkGJXWDSvG9caG30QZK3AYiZB4Qronw3yTi7WscV0XieFhBQ4i7dgAH1IkYcE7gEySI1+hfcEPeJAjiwjGCLJhCT6D7cu+gTXT4AnBZnHFFL0OvD7OfDK7YSCJImQBK0sKrZxPgeyBcjkJlL4VOBNg7W7lM5LTJKDSTpLwV025jDIWsDJTSbgAL6lmIpK3P+spTfEJAgTpOBqJf95G/NzARdfSzoA+tvldPxCwGIChAm4QP6SfuSgIN9hGlD0gwx0Ywkd7YyTME5+8tcrZr9ALtOIQrdgb/HRcZhxcDAOzRQ8LpiDAvOZZgTmgjxSSfbZWiJ+EuQgQX4Knhb4A4iT6csp8Ng2cnr3EmkgAQ4S0EL+98G8BiJMeyIgayrJ7qwlcooxOBhDCwUP2kidIIYZROD+7WSf30vk34zCwSjOkbd8APmLIBnMOCKKrNtBzpFX6e1kBIYRdJCfZWHeEiSLGUpgjo3+sRnmMALDCMJIrSDLmfFkhZC/ixEYhtFKXgXIJmYNea6Z/JUMwxDnPGTYmFeYVcQhyL5mmEMcQ5xP8T4PsoxZR1aC9wXiCENcwuOJMv+SgIcxGE8OmXf5iGe1BbECQSbDVeTFVeglXl+THzvcS6oodM8nsrSQnm5inAzRx/wfC3hIQGZ5Gd88Xke8ruoauqprmAz3psdZWLWTeP+99wmiJ06SKgK5UbKfhJ5fE2OIacc7V2EHs5zC9na8c4kxxESw1wB5pBVcQ58gxhCjmM2kfUGRzcQYBp1n8TeA+0mL0e+eY9FtDDIMsnD8QMBFWozIAK4NDDIMUmQ1aV9hw7cZZBQE+B5p8e5WMM5zFKwQ8JBCGeU+HO4cbuhr8mOHe0mGrNWrQJUbovUNJIPALecouNNpw0pSZGHVTtybHsNVtIR4kcNHufrMLqxAkPEynhxueeqH3PLUFozHzVB2uIfI4WN0PrMLO9zLZNiw0qloKQjJlFHuI2//S2SWlzGS7PUVZK+v4PKWZ+k5cIhEZZT78P7pNVxFSxiO8bhxb95A9vo1tN37BP2NfiZOlxnFlJJEGeU+Co/XkVleRiLy9r9E1upVJCKz3Efh8TpcRUsYi/G4KTxeh6vIy0TZmFIjsJQkyn54DcbjZjxuffmXJOLWl6swHjeJMh43C6t2MlECS52g2SAkm93Ty8ev7CNa38DnXEVeFlbtxFXoJV5meRmuIi9WIEgirLYgXdU1WIEgn8ss97GwaifGnUO87Ifv4zITpdlOhUwhufqb/AQf2YoVCDJU5PAxik8fxVXoJV7W6lX0HDjEWPqb/LTdswE73MsN0RMnuX7iJMUfHiWe8bjJWr2K6ImTjJcimUYQD0nWubMaKxAknh3upXvPPobjKvSSiOAjW7HDvcTrb/TT88YhkknAY0gyqy1I9MRJRtLX6GeiIm8fwwoEGYkVCDIchyeHiTKKhkmivsZmUqW/0c9EZNzlYyIUwkagjyTqb/STKlZbkKkkaJ8BiTBNWIEgo7F7ekkuiRiFi8wQfY1+kknhohHsVtKGZbBbjSCtpI1AzhoDp0kbloHT5g5C/1L4mLSvUAjfQeiMEbCB90mLVy+ghi/9lbSvEHiPQYZBGVh1ChZpMaoG6wiDDINK6LwKvEtajPytlM5LDDLECPYB0r4g6AFiDDHL6XhH4QJpl+cjdcQ4iRH4rAV9UZHXSYAVaKeruoZ40foGRmMF2umqriFetL6BG6L1DXRV1xDPCrQzGivQTld1DfGi9Q0kSuB3Swh+QowwhILTj7dFoIRZSCGcxbXiYsJhYgxDCHxm4KfMWvqbYsJhhjDEWU7wLYVjzDKKts8h9DJxDMNwYj8N9DOLCLrjdugnjoNhvEqkaxs5EaCCWUDhYBmh3QzDMAIfwT0KbzPDKdqSi/0jRmAYRRbXNoMGmKEUjbqwH82nI8oIHIxiD319leTUC7IRyGBGUdvAo6V0/INROBhDLZEr25l3GmQjiDBDKFT6CB1kDA4SsJdrF7eR8z+QB5gBFHaUEaolAQ4StJfIqUqy/YI8BDiZnvoV3VhG6HUS5GAcaon4t5PzPugDIPOYVvQjQdb5CL3LODgYp730tj3L3Dc/w7ECWMo0oOgHGeh9ywidYZyESWim4AVBXuRrTNGflRHazQQJk3SORbdZuPYKVPA1omiTEzYtI9TEJAhJ0kpehY3ZDbKSm0zgV8sJVpMEQpK1kldh43gBuJup1aNo7VysPbdxtZMkEVKkFe+KAdgq6EaQBaSIoo0Cb8zl+oFiwmGSTJgCLeSvU2QtyMNAPpPzCXBKsf8ucMhHx4ekkDDFWvDeacMKAyWKloAUKiwQyAXy+NJlhW6Bj0DbFLkIXHAw0FrK5VNMof8DI/iIM02s0MAAAAAASUVORK5CYII=' + const hh2 = + 'iVBORw0KGgoAAAANSUhEUgAAAIsAAABNCAYAAACWqJdWAAAAAklEQVR4AewaftIAAA8ySURBVO3BC3hU9ZnA4d/3n8mNhBDuJIScMxflErUq1NV6Abe62ivq2tXay/rUrq0+W7XW7nZbra2VutpWpPrY7VZbbHd7kwJSa6l1LdrW1iJSikkgJpMZGAJIgAQCCZM559uhBndKA5yZZEJYz/vi83klZEQsW/FCWNQaj9/CcRC27VtEWYgHjhCKZ+AbUgafzyODz+eRwefzyODzeWTw+Twy+HweGXw+jww+n0cGn88jg8/nkcHn8yiI74Swuba2rDsdPEuEmWCmqzIFoxUooxXZL6LdAjtQbQY2OoH0H+qTyV0MoSC+EatxglVNUeBqQed3O5yNUKIcpCCA8meCgoJykHCQcYrcpurQWhGeAv57RnvbRgYpiG/EaZpqnYOaz6K8E9SQHwPMVmU2cEdTTej3it47sz2+QsAlD0F8I0bj5MgpEtCv4+qFDDXlbEGWbagONzaK3jKrve2X5CjIELEsqzqo5j0YfTvKKcBUYAyQArqAFpR1KvpMynGeSiaTPQw/CdeFzwX3nSKcDcwAxgKlwF5gB0iT4L7gGPOztra2dQyDV6PRkvR+ZwHq3oISoKB0lihPN1WHlhgpumF6e3MHHgUZpGhd3Sw15k5Ur0A0iHK4YmAiMBHhHEE+XhIIdkUs61uBvpJ7m9ubOyi8QKQu9BFEPwXudAY2GhgNGlbkXcbVBRHLflnRBbFEYhmgFEBjbfgkZ5/zY+B0hteVrtv3tqYp9jUzt8Wfw4MAGeOqqr6AF8KLuzs7V5IRjUZLqkaPuQ+Rx4BTQQzelYK8TQPOx8eNrdq6u7NzHccwtqrqbIFL8UCFRZ0ZZJxk26ePHVO1EuGjwARyUy3IVeOqquaNHT/u2d27d3cxhBqrQxeIsgqwOB6E0Yh8+BOV45IP7d29lmMw5CE6NVqrfenfCXorECB/lSiLw5Z9PyAMsahlXecqLwKnMThzxXHWRC3rTIbIhprQewWeBio5voyqPtpUE/4Mx2DIUTgcPlmD6d8DZzBEBD4ZrrP/hSEUsUJ3KPIIUMyQkAmK/DwSiUxjkBqmht+usAQoYaRQvaepJvQJjsKQC2WaOO6zwFSGmAgLQqHQaQyBgOqtoHcx9CbR53yLQWiaHD7VuLoMpYiRRvl6Y7V1OUdgyM0VwFQKI2AcvY8hIZ+gUIRLIrZ9KXlomFhfgdHHgdGMUIJZ/OqkaIQBGEYS4ZKTLGsmI53LLeRBAvsWAdMZ2SrTAecHCgEOE2TwOgWWqspzisRQeo1otYueI8K1QDU5UJGrgTsZemtBloK7Rt1Ah0i6WI05WZT3AJcBglfCxbW1teOSyeQuPGqoDp0v8BFODG/dUBO6gfa2h8gSZFDka47o3fF4vJO/9tPa2tovlQSDD6Bcj0cKFwJ3MnQ2IXysNR5fyV/7LfCd8LTwHDHuT4A6vDElgcBcYBkeKJgN8DAnEmXBxpqTfzi9vbmDfob8uAjvb0203RaPxzs5gmQy2dMaj38c+AFeKacxZPSVA076jNZ4fCVHEdsce0kD5mKFfXikKqfiUVO1NR84hRNLpdJ3E1kMeRDhS63x+A/xRh3hViCFN2Pq6urGMnh7NRB4VzKZ3IUHsVisWdCFeCQQwiMhcDsnIIWbNkyYPpp+QXK3lWDwHnIQj8e3RSz7BWAeHhhjJgO7GRT5SiwW20QOBJYq3I4XwhQ8aJhqn46rZ3IUlVfMJzh+PIekd+5kz9InCIwZw7iPXceoc89BSktItcTYs3wF3c/8ClQZc/X7CIweTbbdj/0XmkoxkDFXv4/A6NFk6/zhj3H3djMgZYwGD/w9sJiMIDnTR1taWg6QM30FZB4eBFXLGBw30Ff0DXJUUlHR0Nu9D4/K8EBccy0oRzP+xuspmTWTQw40NrH/17/FemIJRXYdh5SeegqVl7+XtovexYHGJibcehNFtVPJ1vWjJTipFAOZcOtNFNVOJdvelU/j7u3miESuBRaTYciRuoEV5EFFtuCVyBgG5/fN7c0d5KihoSEFbMebSrwQ993kSoTqB++nyK7jcKlXWzjQ2MQwOn+tbVeRYciJpsdOGvtH8uHSw3ARVpMnhf14IGA4hqYpti0qEXJUMnMG5Recx0C6lixjmJmSFPPIMORAkJY1a9b0kQcxdOGVI5UMgsIG8mRgDx4oWs4xiJhzGaTedevpenwpB5o2gCp7lq1guAlyLhlBcqBokmGgATUMhupmCk6KOAYXnSHkb/vnv8TuR77DIaWnn0ZfcgvHwQwyDDmRDvKkqnsYJgGRDvLkKl0MEYEZ5CkVT7D70cVk6/3jnzguXGaQYciFcoA8iYjL8DlAnkRQhs5E8rT3Zz8HVUYEw0QyDLkwHOAE4Bizn5GhkjylWmIMNQkGyItSSYYhNz2cAFzXTTEyGPKU3tFBPqSkmCMxo8rJk5Bh8BVSijxpby/5kNJSjkiEvAh9ZBh8hSPSzQhiRleQF6WbDIOvcFQ3kye3aw/5CFRVMZDg5EnkTdlMhsFXSBspIHdvN4crsqYxkPLzzyVvho1kGHwFI8IrFFC6o4PDVX3w/SBCtuJImIm3f4a8ufoKGUF8BeOYvt8Yp8gFDAXQu2495eefS7byC87DWvYjupY+gdvZRdlbz2TMNVdhysoYhOfJCOIrmPpkcldTdWgtMJsC6Hp8KeNvvB6MIVvZWXMoO2sOh+uLb6LIriNHPe748hfYBgZfYYksoUBSr7bQ8bVFHJPr8tpdX6b7mf8hV6KsqG9oSJFh8BWUManvAUqBdCx8kK0330Z6+3YG0vPSyyQuv4pd//EI+XCR79IviK+gpieTWzZUh59Q9DKOIHHZPyAmQDZn3z686np8KXuWr6DszDMomTUDCRbhdHbSs3oNqXiCQ1778lfo+Ooisjnd3RyJirbO3Nr2C/oF8RWcIneDXsYRuN37GCztS7P/xdXsf3E1R6K9vTi9vXgn/y7g0M/gK7iZW1vXoKzgRKK06NhR3yWLwTcsVLgJ6OEEoYYb6xsaUmQx+IbFrK1tCUU+xwlA4Huz2tt+yWEMvmEzc2vsAYQnGdk2OulRNzIAg2/YCKhr+v4R2MjItMdV54r6HQ3dDMDgG1b1yeSutJiLQbYwsvSImHfWb9vUyBEYfMPu1PbWzW5A5gFtjAx7xDXvntHe+luOwuA7LuqTrS1uWt8GrOV4Era6RufO2N76LMcgZNgZeJBOp/ckk8ld5KGmpmZUcXHxJDxIpVKvtbe37ydLNBqtTKfT4/DAjtvJVaxKkwfbtqcApRxDsC+YbtnSkmSQ2my7tCfF/aJyA8PvF0Wu+XB0e+treCD4RoSmyeF3Y/RBwKbABDpd5faZ29oeFlA8Enwjxuba2rLudPDTiNwEjGfo9SqyuNiVO6PbW18jR4JvxGmYWF8hwZ5/Evgo6CwGTbaA+13t0wdndSS2kifBN6I11FhnGALzUZ0HnAMUc2wKvIywShzz1PTtrasEXAZJ8J0wXmJ2UUXNrjAwXZUpIBUIleLqPpBu0B0S4NVR0tc8LZnsoRAilr0qWhe6ksPYtj0lYtmrIrZ9KQUSse1LI1boSfpFrNCTESv0zwwgYtmronWhKxki9RPrKyLTIqfg88Twurmu0VoOIyIlwFxgCoUzBfQ83qDnIRplYHNdo7UMkd5R+17UgHMRPk8MR6GqXfz/NhWfZ0FyZGcEVD8FzBBks2NkUVtb2zr6RevqZqnIDapyshFNKea50opRX29oaEiREY1GJ2o6fRsub0H0T7iyGVFyFbGsy8FEWxNtX6Ff2LKuEcy41kTbQ6FQ6Cxx9FLRwFLEuU0MNa7Lb3rTqa+2t7fvj9bZ9yiUiTI/all7WxKJR4FA1LKudUXmi4oR3KdaEolvAg4ZEdt+QGC5wo2qsqc4nfrXDVu27ORNwpCDSG0kGlBdjchcMH9QpM64+rtQKHQWGbZtz1AxL4GcKSIvKdINel9P9/57yLBtu1T70r9B9RqBtYicraL3kA+RuYJ+gCyi8negV5JhVGeJ8CmMswqhRJUOEb5QVlS0iIOEMYCAFKlKORlhK/RNRR4S1W2i+qrCvVEr9BiHKDer8iNV5gh6UbfIft5EgvQTZWHEsheSTflLAecukJ09qdTZ7e3t+wGJWNbTRvkqcEEQrlZ0Z2lFxYUNDQ0pMiJ1togwl4yg6gcUwgHV6c2bEjEgELHs54F6sik3Ryz7ZgavUtHrYonEt8mI2PZeXHkHGS2J+I0Ry74G0UdaE/HFkbq62aDXKXplLJH4CRlhy3oG9MnwtPCi2ObYal7XFEvELwQM4PAmEuT/LFfhObKI6jiQO+gncImiPysvLj75JNvmIFflD6h+xrbt0pZ4/AuzZ89esGbNmr5oNDrRTblvQdzJiFbyZ/K3orzcvCkR43WOoN9XZAFZVHkewzIOI8pCchR0nF/QT6FJhKsYgIhcouDgBhIn2fbpvG6Lq/SK0XnAav5Mfwoo4PAmE6SfCs/F4vEHyGJnBJQ76KcwDuRDrvIh3qAcFOwLTjht8uTdnTt33h+x7Mu0Lz1JRLtBulDSHCRMVGEbWVRkC8pfEMPa1nj8AQ4TseyF5KgmGd3eTJJjUZFJKAEx7mpX+QuKVnOIyE7epILkRNMi8vmWePweBhCxrP9E9f2qcjuueTaWjDVGbPt+lPlkuKpdqEwii6qWC0LOVNKKVpBFRIKKkm0Vq9J4INCrsLM1EZ+Ab0CGnMhq1+VSsoTr7Dsjlv0CEACZi8qS2Kb4olgyth5wVPkb+gnmRRFOj0ajlfQTlfPJTycwDjC8zig6hxwplHGQKy8B4yN1dbPpN33atJqIbTeHLesKfATJhXCfwLKIZX1LNfAYuLNF+BzCNwBHhY0C74ja9iWuY3Ya496gMAfYQ8YBp+/bpYHgv2lfennUtr+oqnOAjwLd5MxdAzI2Umc/rKI/FZEPo4SAbXime0XlQ2Hb7iwpH/WT3u59jYj5cdiyPm2M6U6r3o0yIdhX8jw+DBkC61DdwWFKHCclsE4c2UVGazy+XNEPAvNE3F+LcBfCw6Xl5Z8mwzjOJ0VJqLJSjPMrRV1F3yuwKRQKTU4mk7tAL0aoUOVXINejfBZ0PW/Q9ahsYQAC61DdQUZrIrES9G6EawSWo7ggtyvSQoY4sktgHdlUd4Cu5w3mXiAsyscaGhpSkg5egrJekO+rqytRHEEvam5v7iBDYJ04sgufz3d0/wv/SPsTabkxNQAAAABJRU5ErkJggg==' + + const hhLogo1 = await Jimp.read(Buffer.from(hh1, 'base64')) + const hhLogo2 = await Jimp.read(Buffer.from(hh2, 'base64')) + + for (const imgData of model.images) { + if (imgData.width < 15 || imgData.height < 15) { + continue + } + const img = await Jimp.read(imgData.pngBuffer) + const d1 = Jimp.diff(hhLogo1, img) + const d2 = Jimp.diff(hhLogo2, img) + if (d1.percent < 0.01 || d2.percent < 0.01) { + imgData.potentialAvatar = false + return true + } + } + return false +} + +export function parseHeadHunter (text: RekoniModel, resume: ReconiDocument): void { + const sorted = [...text.lines] + sorted.sort((a, b) => b.height - a.height) + + const title = sorted.shift() + if (title !== undefined) { + const titems = getLineItems(title.items, true).filter((t) => t.length > 0) + if (titems.length >= 2) { + resume.lastName = titems[0] + resume.firstName = titems.slice(1).join(' ') + } + if (titems.length === 1) { + const ti = titems[0].split(' ') + if (ti.length >= 2) { + resume.lastName = ti[0] + resume.firstName = ti.slice(1).join(' ') + } + } + } + + for (let i = 0; i < 3; i++) { + const position = sorted.shift() + if (position !== undefined) { + const positionText = getLineItems(position.items, true).join('').trim() + if (positionText.length > 0 && positionText.match(/\d\d+/) === null) { + resume.title = positionText + break + } + } + } + + let skills = false + let skillsHeight = 0 + let parseValues = true + for (const t of text.lines) { + const line = getLineItems(t.items, false).join('').trim() + if (line.toLowerCase().includes('опыт работы')) { + parseValues = false + continue + } + if (parseValues) { + if (resume.phone === undefined) { + const phone = parsePhoneNumber(line) + if (phone !== undefined) { + resume.phone = phone.formatInternational() + } + } + + if (resume.email === undefined) { + for (const ll of line.split(' ')) { + const parsedddr = addrs.parseOneAddress(ll) + if (parsedddr != null) { + resume.email = ll + break + } + } + } + + const twoDots = line.indexOf(':') + if (twoDots > 0) { + const first = line.slice(0, twoDots).trim().toLowerCase() + const value = line.slice(twoDots + 1).trim() + if (first === 'проживает' || first === 'residential address' || first === 'reside in') { + resume.city = value + } else if (first === 'skype') { + resume.skype = value + } else if (first === 'linkedin') { + resume.linkedin = value + } else if (first === 'github') { + resume.github = value + } else if (first === 'facebook') { + resume.facebook = value + } else if (first.startsWith('другой сайт')) { + if (value.includes('github.com')) { + resume.github = value + } + if (value.includes('linkedin.')) { + resume.linkedin = value + } + } + } + } + if (t.height !== skillsHeight) { + if (skillsHeight === -1) { + skillsHeight = t.height + } else { + skills = false + } + } + if (t.items.length > 0) { + const first = t.items[0].toLowerCase() + if (first === 'навыки' || first === 'skills') { + skills = true + skillsHeight = t.height + handleSkills(t.items.slice(1), resume) + if (resume.skills.length === 0) { + // No skills on this line, skipping it. + skillsHeight = -1 + } + } else if (skills) { + handleSkills(t.items, resume) + } + } + } + parseSkillsRaw(resume, text) +} diff --git a/services/rekoni/src/index.ts b/services/rekoni/src/index.ts new file mode 100644 index 0000000000..5386144969 --- /dev/null +++ b/services/rekoni/src/index.ts @@ -0,0 +1,20 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// eslint-disable-next-line +require('dotenv').config() +// eslint-disable-next-line +import { startServer } from './server' +void startServer() diff --git a/services/rekoni/src/jwt.ts b/services/rekoni/src/jwt.ts new file mode 100644 index 0000000000..da5426ef7c --- /dev/null +++ b/services/rekoni/src/jwt.ts @@ -0,0 +1,21 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { encode as jwtEncode, decode as jwtDecode } from 'jwt-simple' + +import config from './config' + +export const encode = (data: any): string => jwtEncode(data, config.Secret) +export const decode = (data: string): any => jwtDecode(data, config.Secret) diff --git a/services/rekoni/src/linkedin.ts b/services/rekoni/src/linkedin.ts new file mode 100644 index 0000000000..e8e2ff95e7 --- /dev/null +++ b/services/rekoni/src/linkedin.ts @@ -0,0 +1,152 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { findPhoneNumbersInText } from 'libphonenumber-js' +import { parseSkillsRaw } from './skills' +import { ReconiDocument, RekoniItem, RekoniModel } from './types' +import { getLineItems, handleSkills } from './utils' + +export async function isLinkedin (model: RekoniModel): Promise { + return model.author === 'LinkedIn' +} + +export function parseLinkedin (text: RekoniModel, resume: ReconiDocument): void { + let titleIndex = -1 + let titleSize = -1 + for (let index = 0; index < text.lines.length; index++) { + const line = text.lines[index] + if (titleSize < line.height) { + titleSize = line.height + titleIndex = index + } + } + const leftBlock = text.lines.slice(0, titleIndex) + const main = text.lines.slice(titleIndex) + const title = main.shift() + if (title !== undefined) { + const titems = getLineItems(title.items, true).filter((t) => t.length > 0) + if (titems.length >= 2) { + resume.lastName = titems[titems.length - 1] + resume.firstName = titems.slice(0, titems.length - 1).join(' ') + } + if (titems.length === 1) { + const ti = titems[0].split(' ') + if (ti.length >= 2) { + resume.lastName = ti[ti.length - 1] + resume.firstName = ti.slice(0, ti.length - 1).join(' ') + } + } + } + + const position = main.shift() + if (position !== undefined) { + const positionText = getLineItems(position.items, true).join('').trim() + if (positionText.length > 0 && positionText.match(/\d\d+/) === null) { + resume.title = positionText + } + } + const location = main.shift() + if (location !== undefined) { + const locationText = getLineItems(location.items, true).join('').trim() + if (locationText.length > 0 && locationText.match(/\d\d+/) === null) { + resume.city = locationText + } + } + + let leftCaptionSize = -1 + for (const t of leftBlock) { + leftCaptionSize = Math.max(t.height, leftCaptionSize) + } + + const contactBlock = getBlock(leftCaptionSize, leftBlock, ['способы связаться', 'contact']) + const skillsBlock = getBlock(leftCaptionSize, leftBlock, ['top skills', 'основные навыки']) + + for (let index = 0; index < contactBlock.length; index++) { + if (resume.phone === undefined) { + const t = contactBlock[index] + const line = getLineItems(t.items, false).join('').trim() + const trimmed = tryExtractNumbers(line) + if (trimmed !== null) { + const phone = findPhoneNumbersInText(trimmed[0]) + if (phone.length > 0) { + resume.phone = phone[0].number.formatInternational() + break + } + } + } + } + + for (const line of text.annotations) { + const val = line.value + if (resume.email === undefined) { + const split = val.split(':') + if (split.length >= 2 && split[0] === 'mailto') { + split.shift() + resume.email = split.join(':') + } + } + if (val.includes('linkedin')) { + resume.linkedin = val.split('?')[0] + } + if (val.includes('github')) { + resume.github = val + } + + if (val.includes('facebook')) { + resume.facebook = val + } + if (val.includes('twitter')) { + resume.twitter = val + } + } + + // skills parse + for (const t of skillsBlock) { + if (t.items.length > 0) { + handleSkills(t.items, resume) + } + } + parseSkillsRaw(resume, text) +} + +function tryExtractNumbers (text: string): RegExpMatchArray | null { + const regex = /[\d()+\-./\s]{7,20}/g + const matches = text.match(regex) + return matches +} + +function getBlock (captionSize: number, items: RekoniItem[], searchStrings: string[]): RekoniItem[] { + let startIndex: number | undefined + let endIndex: number | undefined + for (let index = 0; index < items.length; index++) { + const element = items[index] + if (element.height === captionSize) { + if (startIndex === undefined) { + const line = getLineItems(element.items, false).join('').trim().toLowerCase() + const match = searchStrings.some((p) => line.includes(p)) + if (match) { + startIndex = index + } + } else { + endIndex = index + break + } + } + } + if (startIndex !== undefined) { + return items.slice(startIndex + 1, endIndex) + } + return [] +} diff --git a/services/rekoni/src/podborio.ts b/services/rekoni/src/podborio.ts new file mode 100644 index 0000000000..5a8497279c --- /dev/null +++ b/services/rekoni/src/podborio.ts @@ -0,0 +1,145 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { parseSkillsRaw } from './skills' +import { ReconiDocument, RekoniModel } from './types' +import { getLineItems } from './utils' + +export function isPodborIO (text: RekoniModel): boolean { + for (const ann of text.annotations) { + if (ann.type === 'link') { + const url = ann.value + if (url.includes('app.podbor.io')) { + return true + } + } + } + return false +} + +export function parsePodborIO (text: RekoniModel, resume: ReconiDocument): void { + for (const ann of text.annotations) { + if (ann.type === 'link') { + const url = ann.value + if (url.includes('linkedin.')) { + resume.linkedin = url + } + if (url.includes('github.')) { + resume.github = url + } + } + } + const sorted = [...text.lines].filter((a) => a.items.map((d) => d.trim()).join('').length > 0) + sorted.sort((a, b) => b.height - a.height) + const title = sorted.shift() + if (title !== undefined) { + const titems = getLineItems(title.items, true).filter((t) => t.length > 0) + if (titems.length >= 2) { + resume.firstName = titems[0] + titems.slice(2).join('') + resume.lastName = titems[1] + } else if (titems.length === 1) { + const ti = titems[0].split(' ') + if (ti.length >= 2) { + resume.firstName = ti[0] + ti.slice(2).join(' ') + resume.lastName = ti[1] + } + } + } + let socials = false + let skills = false + for (const t of text.lines) { + const line = getLineItems(t.items, false).join('').trim() + const twoDots = line.indexOf(':') + let first = '' + let value = '' + if (twoDots > 0) { + first = line.slice(0, twoDots).trim().toLowerCase() + value = line.slice(twoDots + 1).trim() + } else { + first = t.items.length > 0 ? t.items[0].trim().toLowerCase() : '' + value = t.items.length > 0 ? getLineItems(t.items.slice(1), false).join('').trim() : '' + } + parseAttribute(resume, first, value) + + if (first === 'соц.сети') { + socials = true + skills = false + continue + } + if (first === 'навыки') { + socials = false + skills = true + continue + } + if (first === 'опыт работы') { + skills = false + socials = false + continue + } + + if (skills) { + for (const tt of t.items) { + const ttv = tt.trim() + if (ttv.length > 0 && resume.skills.findIndex((it) => it.toLowerCase() === ttv.toLowerCase()) === -1) { + resume.skills.push(ttv) + } + } + } + + if (socials) { + for (const tt of t.items) { + if (resume.linkedin === undefined && tt.includes('linkedin.')) { + resume.linkedin = tt + } + if (resume.github === undefined && tt.includes('github.')) { + resume.github = tt + } + if (resume.facebook === undefined && (tt.includes('facebook.') || tt.includes('fb.com'))) { + resume.facebook = tt + } + if (resume.twitter === undefined && tt.includes('twitter.')) { + resume.twitter = tt + } + } + } + } + + parseSkillsRaw(resume, text) +} +function parseAttribute (resume: ReconiDocument, first: string, value: string): void { + if (first === 'город') { + resume.city = value + } + if (first === 'текущие позиции') { + const pos = value.indexOf(',') + if (pos !== -1) { + resume.title = value.substring(pos + 1).trim() + } else { + resume.title = value + } + } + if (first === 'e-mail') { + resume.email = value + } + if (first === 'skype') { + resume.skype = value + } + if (first === 'телефон') { + resume.phone = value + } + if (first === 'телеграм') { + resume.telegram = value + } +} diff --git a/services/rekoni/src/process.ts b/services/rekoni/src/process.ts new file mode 100644 index 0000000000..09c71c20da --- /dev/null +++ b/services/rekoni/src/process.ts @@ -0,0 +1,79 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type TypedArray } from 'pdfjs-dist/types/src/display/api' +import { extractData } from './extractpdf' +import { parseGenericResume } from './generic' +import { isHeadHunter, parseHeadHunter } from './headhunter' +import { isPodborIO, parsePodborIO } from './podborio' +import { type ReconiDocument, type RekoniModel } from './types' +import { isLinkedin, parseLinkedin } from './linkedin' +import { capitalizeName } from './utils' + +export async function extractDocument ( + data: string | TypedArray +): Promise<{ resume: ReconiDocument, model: RekoniModel }> { + const resume: ReconiDocument = { + format: 'unknown', + firstName: '', + lastName: '', + skills: [] + } + + const text = await extractData(data) + + let format: 'headhunter' | 'unknown' | 'podbor' | 'linkedin' = 'unknown' + + if (isPodborIO(text)) { + format = 'podbor' + } else if (await isHeadHunter(text)) { + format = 'headhunter' + } else if (await isLinkedin(text)) { + format = 'linkedin' + } + resume.format = format + + const curWh = { width: 0, height: 0 } + for (const imgData of text.images) { + if (imgData.width > 5 * imgData.height) { + // skip, non avatar images. + continue + } + if (!imgData.potentialAvatar) { + continue + } + if (resume.avatar === undefined || (imgData.width > curWh.width && imgData.height > curWh.height)) { + resume.avatar = imgData.buffer.toString('base64') + resume.avatarName = imgData.name + resume.avatarFormat = imgData.format + curWh.width = imgData.width + curWh.height = imgData.height + } + } + + if (format === 'podbor') { + parsePodborIO(text, resume) + } else if (format === 'headhunter') { + parseHeadHunter(text, resume) + } else if (format === 'linkedin') { + parseLinkedin(text, resume) + } else { + // Try parse generic resume + parseGenericResume(text, resume) + } + resume.lastName = capitalizeName(resume.lastName) + resume.firstName = capitalizeName(resume.firstName) + return { resume, model: text } +} diff --git a/services/rekoni/src/server.ts b/services/rekoni/src/server.ts new file mode 100644 index 0000000000..0b08d8f291 --- /dev/null +++ b/services/rekoni/src/server.ts @@ -0,0 +1,246 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import bodyParser from 'body-parser' +import cors from 'cors' +import express from 'express' +import formData from 'express-form-data' +import { type IncomingHttpHeaders, type Server } from 'http' +import os from 'os' +import { type Readable } from 'stream' +import config from './config' +import { ApiError } from './error' +import { extract } from './extractors' +import { parseGenericResume } from './generic' +import { decode } from './jwt' +import { extractDocument } from './process' +import { type ReconiDocument } from './types' +// eslint-disable-next-line @typescript-eslint/no-var-requires + +const extractToken = (header: IncomingHttpHeaders): any => { + try { + return header.authorization?.slice(7) ?? '' + } catch { + return undefined + } +} + +export const startServer = async (): Promise => { + const app = express() + + app.use(cors()) + app.use(express.json({ limit: '50mb' })) + app.use(express.text({ limit: '50mb' })) + app.use(express.raw({ limit: '50mb' })) + app.use(bodyParser.json()) + app.use( + bodyParser.text({ + type: 'text/plain' + }) + ) + app.use( + bodyParser.raw({ + type: 'application/octet-stream' + }) + ) + + const options = { + uploadDir: os.tmpdir(), + autoClean: true + } + + // parse data with connect-multiparty. + app.use(formData.parse(options)) + // delete from the request all empty files (size == 0) + app.use(formData.format()) + // change the file objects to fs.ReadStream + app.use(formData.stream()) + // union the body and the files + app.use(formData.union()) + + const ops = new Map>() + let idx = 0 + // Allow to execute 30 per second. + const extractQueue = { + add: async (exec: () => Promise): Promise => { + if (ops.size > 100) { + await Promise.any(ops.values()) + } + const id = idx++ + const p = exec() + ops.set( + id, + p.then(() => { + ops.delete(id) + }) + ) + + return await p + } + } + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + app.post('/recognize', async (req, res) => { + const token = extractToken(req.headers) + decode(token) + const params = req.body + console.log('recognize from', params.fileUrl) + + const contentType = req.body.type as string + const name = req.body.name as string + + const bodyStream = req.body.file as Readable + + const temp: Buffer[] = [] + const body = await new Promise((resolve) => { + bodyStream.on('data', function (d) { + temp.push(d) + }) + bodyStream.on('end', function () { + resolve(Buffer.concat(temp)) + }) + }) + + res.set('Cache-Control', 'no-cache') + + if (contentType === 'application/pdf') { + await extractQueue.add(async () => { + res.set('Cache-Control', 'no-cache') + try { + const { resume: doc } = await extractDocument(body) + res.status(200) + res.json(doc) + } catch (err: any) { + res.status(400) + res.json({ error: JSON.stringify(err) }) + } + }) + } else { + await extractQueue.add(async () => { + res.set('Cache-Control', 'no-cache') + try { + const { content, error } = await extract(name, contentType, body) + if (error !== undefined) { + res.status(400) + res.json({ error: JSON.stringify(error) }) + return + } else { + res.status(200) + } + + const resume: ReconiDocument = { + format: 'unknown', + firstName: '', + lastName: '', + skills: [] + } + + const textContent = content + .split(/ |\t|\f/) + .filter((it) => it) + .join(' ') + .split(/\n+/) + .join('\n') + .trim() + + parseGenericResume( + { + author: undefined, + annotations: [], + images: [], + lines: textContent.split('\n').map((it) => ({ + items: [it], + height: 0, + widths: [] + })) + }, + resume + ) + res.status(200) + res.json(resume) + } catch (err: any) { + res.status(400) + res.json({ error: JSON.stringify(err) }) + } + }) + } + }) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + app.post('/toText', async (req, res) => { + const token = extractToken(req.headers) + decode(token) + const name = req.query.name as string + const contentType = req.query.type as string + + try { + const body = typeof req.body === 'string' ? Buffer.from(req.body, 'base64') : (req.body as Buffer) + + res.set('Cache-Control', 'no-cache') + await extractQueue.add(async () => { + const { matched, content, error } = await extract(name, contentType, body) + if (error !== undefined) { + res.status(400) + } else { + res.status(200) + } + res.json({ + matched, + content, + error + }) + }) + } catch (err: any) { + res.status(400) + res.json({ error: JSON.stringify(err) }) + } + }) + + app.use((_req, res, _next) => { + res.status(404).send({ message: 'Not found' }) + }) + + app.use((err: any, _req: any, res: any, _next: any) => { + if (err instanceof ApiError) { + res.status(400).send({ code: err.code, message: err.message }) + return + } + + res.status(500).send({ message: err.message }) + }) + + const server = listen(app, config.Port) + + const shutdown = (): void => { + server.close() + } + + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + process.on('uncaughtException', (e) => { + console.error(e) + }) + process.on('unhandledRejection', (e) => { + console.error(e) + }) +} + +export function listen (e: express.Express, port: number, host?: string): Server { + const cb = (): void => { + console.log(`Rekoni service has been started at ${host ?? '*'}:${port}`) + } + + return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb) +} diff --git a/services/rekoni/src/skills.ts b/services/rekoni/src/skills.ts new file mode 100644 index 0000000000..89129e8250 --- /dev/null +++ b/services/rekoni/src/skills.ts @@ -0,0 +1,40 @@ +import { getCategories } from '@anticrm/skillset' +import { ReconiDocument, RekoniModel } from './types' +import { getLineItems } from './utils' + +const categories = getCategories() +export const possibleSkills = new Set( + categories + .map((it) => it.skills) + .reduce((arr, val) => { + arr.push(...val) + return arr + }, []) +) +export const lowePossibleSkills = new Set( + categories + .map((it) => it.skills) + .reduce((arr, val) => { + arr.push(...val) + return arr + }, []) + .map((it) => it.toLowerCase()) +) + +export function parseSkillsRaw (resume: ReconiDocument, text: RekoniModel): void { + if (resume.skills.length === 0) { + // try to match skills from set of possible ones. + for (const tt of text.lines) { + const line = getLineItems(tt.items, false).join() + for (const skill of possibleSkills) { + if ( + skill.length > 3 && + line.includes(skill) && + resume.skills.findIndex((it) => it.toLowerCase() === skill.toLowerCase()) === -1 + ) { + resume.skills.push(skill) + } + } + } + } +} diff --git a/services/rekoni/src/types.ts b/services/rekoni/src/types.ts new file mode 100644 index 0000000000..5230292947 --- /dev/null +++ b/services/rekoni/src/types.ts @@ -0,0 +1,72 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/** + * @public + */ +export interface ReconiDocument { + format: string + firstName: string + lastName: string + title?: string + avatar?: string + avatarName?: string + avatarFormat?: string + email?: string + phone?: string + city?: string + skype?: string + linkedin?: string + gmail?: string + github?: string + facebook?: string + telegram?: string + twitter?: string + skills: string[] +} + +/** + * One item per line + */ +export interface RekoniItem { + // Text representation + items: string[] + + // Individual widths + widths: number[] + + // Max height + height: number +} +export interface RekoniAnnotation { + type: string + value: string +} +export interface RekoniModelImage { + name: string + width: number + height: number + buffer: Buffer + pngBuffer: Buffer + format: string + potentialAvatar: boolean +} +export interface RekoniModel { + // Lines in document. + lines: RekoniItem[] + annotations: RekoniAnnotation[] + images: RekoniModelImage[] + author: string | undefined +} diff --git a/services/rekoni/src/utils.ts b/services/rekoni/src/utils.ts new file mode 100644 index 0000000000..f72ab3f4d6 --- /dev/null +++ b/services/rekoni/src/utils.ts @@ -0,0 +1,60 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { ReconiDocument } from './types' + +/** + * @public + */ +export function getLineItems (items: string[], trim: boolean): string[] { + return items.map((t) => { + const r = t.split('\u00A0').join(' ') + if (trim) { + return r.trim() + } + return r + }) +} + +/** + * @public + */ +export function isPrivateCharCode (code: number): boolean { + return code > 0xe000 && code < 0xf8ff +} + +export function handleSkills (items: string[], resume: ReconiDocument, check?: (s: string) => boolean): void { + for (const tt of items) { + if (tt.trim().length > 1) { + const v = tt.trim() + if ( + (check === undefined || check(v)) && + resume.skills.findIndex((it) => it.toLowerCase() === v.toLowerCase()) === -1 + ) { + resume.skills.push(v) + } + } + } +} + +export function capitalizeName (str: string): string { + const words = str.split(/\s+/) + const capitalizedWords = words.map((word) => { + const firstLetter = word.charAt(0) + const restOfWord = word.slice(1) + return firstLetter.toLocaleUpperCase() + restOfWord.toLocaleLowerCase() + }) + return capitalizedWords.join(' ') +} diff --git a/services/rekoni/tsconfig.json b/services/rekoni/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/rekoni/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/ses/pod-ses/.eslintrc.js b/services/ses/pod-ses/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/ses/pod-ses/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/ses/pod-ses/.npmignore b/services/ses/pod-ses/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/ses/pod-ses/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/ses/pod-ses/Dockerfile b/services/ses/pod-ses/Dockerfile new file mode 100644 index 0000000000..f1d88c2c0c --- /dev/null +++ b/services/ses/pod-ses/Dockerfile @@ -0,0 +1,8 @@ +FROM node:20-alpine + +WORKDIR /usr/src/app + +COPY bundle/bundle.js ./ + +EXPOSE 8089 +CMD [ "node", "bundle.js" ] diff --git a/services/ses/pod-ses/build.sh b/services/ses/pod-ses/build.sh new file mode 100755 index 0000000000..3346c7147e --- /dev/null +++ b/services/ses/pod-ses/build.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# +# Copyright © 2023 Hardcore Engineering Inc. +# +# Licensed under the Eclipse Public License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may +# obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# See the License for the specific language governing permissions and +# limitations under the License. +# + +rushx bundle +rushx docker:build +rushx docker:push diff --git a/services/ses/pod-ses/config/rig.json b/services/ses/pod-ses/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/ses/pod-ses/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/ses/pod-ses/jest.config.js b/services/ses/pod-ses/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/ses/pod-ses/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/ses/pod-ses/package.json b/services/ses/pod-ses/package.json new file mode 100644 index 0000000000..15026bcbe4 --- /dev/null +++ b/services/ses/pod-ses/package.json @@ -0,0 +1,68 @@ +{ + "name": "@hcengineering/pod-ses", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "_phase:bundle": "rushx bundle", + "_phase:docker-build": "rushx docker:build", + "_phase:docker-staging": "rushx docker:staging", + "bundle": "mkdir -p bundle && esbuild src/index.ts --bundle --platform=node > bundle/bundle.js", + "docker:build": "../../../common/scripts/docker_build.sh hardcoreeng/ses", + "docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/ses staging", + "docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/ses", + "run-local": "cross-env SOURCE=no-reply@bold.ru ts-node src/index.ts", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "cross-env": "~7.0.3", + "@hcengineering/platform-rig": "^0.6.0", + "@types/node": "~20.11.16", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "esbuild": "^0.20.0", + "prettier": "^3.1.0", + "ts-node": "^10.8.0", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@tsconfig/node16": "^1.0.4", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/ws": "^8.5.11", + "eslint-plugin-node": "^11.1.0" + }, + "dependencies": { + "@hcengineering/client": "^0.6.18", + "@hcengineering/client-resources": "^0.6.27", + "@hcengineering/core": "^0.6.32", + "@hcengineering/notification": "^0.6.23", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/server-token": "^0.6.11", + "aws-sdk": "^2.1423.0", + "cors": "^2.8.5", + "dotenv": "~16.0.0", + "express": "^4.19.2", + "got": "^11.8.3" + } +} diff --git a/services/ses/pod-ses/src/config.ts b/services/ses/pod-ses/src/config.ts new file mode 100644 index 0000000000..1a227a82e9 --- /dev/null +++ b/services/ses/pod-ses/src/config.ts @@ -0,0 +1,54 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +interface Config { + Port: number + Source: string + AccessKey: string + SecretKey: string + Region: string +} + +const envMap: { [key in keyof Config]: string } = { + Port: 'PORT', + Source: 'SOURCE', + AccessKey: 'ACCESS_KEY', + SecretKey: 'SECRET_KEY', + Region: 'REGION' +} + +const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined) + +const config: Config = (() => { + const params: Partial = { + Port: parseNumber(process.env[envMap.Port]) ?? 8091, + Source: process.env[envMap.Source], + AccessKey: process.env[envMap.AccessKey], + SecretKey: process.env[envMap.SecretKey], + Region: process.env[envMap.Region] ?? 'us-east-1' + } + + const required: Array = ['Port', 'Source', 'AccessKey', 'SecretKey', 'Region'] + + const missingEnv = required.filter((key) => params[key] === undefined).map((key) => envMap[key]) + + if (missingEnv.length > 0) { + throw Error(`Missing env variables: ${missingEnv.join(', ')}`) + } + + return params as Config +})() + +export default config diff --git a/services/ses/pod-ses/src/error.ts b/services/ses/pod-ses/src/error.ts new file mode 100644 index 0000000000..585b319bb2 --- /dev/null +++ b/services/ses/pod-ses/src/error.ts @@ -0,0 +1,23 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export class ApiError extends Error { + constructor ( + readonly code: string, + readonly message: string + ) { + super(message) + } +} diff --git a/services/ses/pod-ses/src/index.ts b/services/ses/pod-ses/src/index.ts new file mode 100644 index 0000000000..22b45ddd13 --- /dev/null +++ b/services/ses/pod-ses/src/index.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { main } from './main' + +void main() diff --git a/services/ses/pod-ses/src/main.ts b/services/ses/pod-ses/src/main.ts new file mode 100644 index 0000000000..4f27c79901 --- /dev/null +++ b/services/ses/pod-ses/src/main.ts @@ -0,0 +1,76 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import config from './config' +import { createServer, listen } from './server' +import { SES } from './ses' +import { Endpoint } from './types' + +export const main = async (): Promise => { + const ses = new SES() + console.log('SES service has been started') + + const endpoints: Endpoint[] = [ + { + endpoint: '/send', + type: 'post', + handler: async (req, res) => { + const text = req.body?.text + if (text === undefined) { + res.status(400).send({ err: "'text' is missing" }) + return + } + const subject = req.body?.subject + if (subject === undefined) { + res.status(400).send({ err: "'subject' is missing" }) + return + } + const html = req.body?.html + const to = req.body?.to + if (to === undefined) { + res.status(400).send({ err: "'to' is missing" }) + return + } + const receivers = { + to: Array.isArray(to) ? to : [to] + } + try { + await ses.sendMessage({ text, subject, html }, receivers) + } catch (err) { + console.log(err) + } + + res.send() + } + } + ] + + const server = listen(createServer(endpoints), config.Port) + + const shutdown = (): void => { + server.close(() => { + process.exit() + }) + } + + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + process.on('uncaughtException', (e) => { + console.error(e) + }) + process.on('unhandledRejection', (e) => { + console.error(e) + }) +} diff --git a/services/ses/pod-ses/src/server.ts b/services/ses/pod-ses/src/server.ts new file mode 100644 index 0000000000..bbd7a32684 --- /dev/null +++ b/services/ses/pod-ses/src/server.ts @@ -0,0 +1,68 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import cors from 'cors' +import express, { Express, NextFunction, Request, Response } from 'express' +import { Server } from 'http' +import { ApiError } from './error' +import { Endpoint, RequestHandler } from './types' + +const catchError = (fn: RequestHandler) => (req: Request, res: Response, next: NextFunction) => { + void (async () => { + try { + await fn(req, res, next) + } catch (err: unknown) { + next(err) + } + })() +} + +export function createServer (endpoints: Endpoint[]): Express { + const app = express() + + app.use(cors()) + app.use(express.json()) + + endpoints.forEach((endpoint) => { + if (endpoint.type === 'get') { + app.get(endpoint.endpoint, catchError(endpoint.handler)) + } else if (endpoint.type === 'post') { + app.post(endpoint.endpoint, catchError(endpoint.handler)) + } + }) + + app.use((_req, res, _next) => { + res.status(404).send({ message: 'Not found' }) + }) + + app.use((err: any, _req: any, res: any, _next: any) => { + if (err instanceof ApiError) { + res.status(400).send({ code: err.code, message: err.message }) + return + } + + res.status(500).send({ message: err.message }) + }) + + return app +} + +export function listen (e: Express, port: number, host?: string): Server { + const cb = (): void => { + console.log(`SES service has been started at ${host ?? '*'}:${port}`) + } + + return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb) +} diff --git a/services/ses/pod-ses/src/ses.ts b/services/ses/pod-ses/src/ses.ts new file mode 100644 index 0000000000..624d9ddc1b --- /dev/null +++ b/services/ses/pod-ses/src/ses.ts @@ -0,0 +1,65 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AWS from 'aws-sdk' +import config from './config' +import { Message, Receivers } from './types' + +export class SES { + private readonly client: AWS.SES + + constructor () { + AWS.config.credentials = { + accessKeyId: config.AccessKey, + secretAccessKey: config.SecretKey + } + this.client = new AWS.SES({ region: config.Region }) + } + + async sendMessage (message: Message, receivers: Receivers, from?: string): Promise { + console.log('send email to', receivers.to) + const params: AWS.SES.Types.SendEmailRequest = { + Source: config.Source, + Destination: { + ToAddresses: receivers.to + }, + Message: { + Subject: { + Data: message.subject + }, + Body: { + Text: { + Data: message.text + } + } + } + } + // if (from !== undefined) { + // params.Source = `${from} <` + config.Source + '>' + // } + if (receivers.cc !== undefined) { + params.Destination.CcAddresses = receivers.cc + } + if (receivers.bcc !== undefined) { + params.Destination.BccAddresses = receivers.bcc + } + if (message.html !== undefined) { + params.Message.Body.Html = { + Data: message.html + } + } + await this.client.sendEmail(params).promise() + } +} diff --git a/services/ses/pod-ses/src/types.ts b/services/ses/pod-ses/src/types.ts new file mode 100644 index 0000000000..b3eec38b8c --- /dev/null +++ b/services/ses/pod-ses/src/types.ts @@ -0,0 +1,38 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { NextFunction, Request, Response } from 'express' + +export interface Receivers { + to: string[] + cc?: string[] + bcc?: string[] +} + +export interface Message { + text: string + subject: string + html?: string +} + +export type RequestType = 'get' | 'post' + +export type RequestHandler = (req: Request, res: Response, next?: NextFunction) => Promise + +export interface Endpoint { + endpoint: string + type: RequestType + handler: RequestHandler +} diff --git a/services/ses/pod-ses/tsconfig.json b/services/ses/pod-ses/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/ses/pod-ses/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/sign/pod-sign/.eslintrc.js b/services/sign/pod-sign/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/sign/pod-sign/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/sign/pod-sign/.npmignore b/services/sign/pod-sign/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/sign/pod-sign/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/sign/pod-sign/Dockerfile b/services/sign/pod-sign/Dockerfile new file mode 100644 index 0000000000..1cd7ec0f03 --- /dev/null +++ b/services/sign/pod-sign/Dockerfile @@ -0,0 +1,9 @@ + +FROM node:20 + +RUN apt-get update && apt-get install dumb-init +WORKDIR /usr/src/app + +COPY bundle/bundle.js ./ + +CMD [ "dumb-init", "node", "bundle.js" ] diff --git a/services/sign/pod-sign/config/rig.json b/services/sign/pod-sign/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/sign/pod-sign/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/sign/pod-sign/debug/branding.json b/services/sign/pod-sign/debug/branding.json new file mode 100644 index 0000000000..0e7f1d64ac --- /dev/null +++ b/services/sign/pod-sign/debug/branding.json @@ -0,0 +1,18 @@ +{ + "localhost:8080": { + "title": "Huly", + "language": "en" + }, + "localhost:8087": { + "title": "Huly", + "language": "en" + }, + "localhost:8081": { + "title": "TraceX", + "language": "en" + }, + "localhost:8088": { + "title": "TraceX", + "language": "en" + } +} diff --git a/services/sign/pod-sign/debug/certificate.p12 b/services/sign/pod-sign/debug/certificate.p12 new file mode 100644 index 0000000000..97c5975118 Binary files /dev/null and b/services/sign/pod-sign/debug/certificate.p12 differ diff --git a/services/sign/pod-sign/jest.config.js b/services/sign/pod-sign/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/sign/pod-sign/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/sign/pod-sign/package.json b/services/sign/pod-sign/package.json new file mode 100644 index 0000000000..62b90f2c40 --- /dev/null +++ b/services/sign/pod-sign/package.json @@ -0,0 +1,75 @@ +{ + "name": "@hcengineering/pod-sign", + "version": "0.1.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "_phase:bundle": "rushx bundle", + "_phase:docker-build": "rushx docker:build", + "_phase:docker-staging": "rushx docker:staging", + "bundle": "mkdir -p bundle && esbuild src/index.ts --bundle --platform=node --keep-names > bundle/bundle.js", + "docker:build": "cross-env DOCKER_EXTRA=--platform=linux/amd64 ../../../common/scripts/docker_build.sh hardcoreeng/sign", + "docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/sign staging", + "docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/sign", + "run-local": "cross-env ts-node src/index.ts", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@tsconfig/node16": "^1.0.4", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/node": "~20.11.16", + "@types/ws": "^8.5.11", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "esbuild": "^0.20.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "prettier": "^3.1.0", + "ts-node": "^10.8.0", + "typescript": "^5.3.3" + }, + "dependencies": { + "@hcengineering/server-token": "^0.6.11", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/server": "^0.6.4", + "@hcengineering/core": "^0.6.32", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/client": "^0.6.18", + "@hcengineering/client-resources": "^0.6.27", + "@hcengineering/server-client": "^0.6.0", + "cors": "^2.8.5", + "dotenv": "~16.0.0", + "express": "^4.19.2", + "@signpdf/signpdf": "^3.2.4", + "@signpdf/placeholder-pdf-lib": "^3.2.4", + "@signpdf/signer-p12": "^3.2.4", + "@signpdf/utils": "^3.2.4", + "node-forge": "^1.3.1", + "pdf-lib": "^1.12.0", + "ws": "^8.18.0", + "mongodb": "^6.8.0" + } +} diff --git a/services/sign/pod-sign/src/branding.ts b/services/sign/pod-sign/src/branding.ts new file mode 100644 index 0000000000..c95e3ccc32 --- /dev/null +++ b/services/sign/pod-sign/src/branding.ts @@ -0,0 +1,31 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { IncomingHttpHeaders } from 'http' + +export interface Branding { + title?: string +} + +export type BrandingMap = Record + +export const extractBranding = (brandings: BrandingMap, headers: IncomingHttpHeaders): Branding | null => { + let host: string | undefined + const origin = headers.origin ?? headers.referer + if (origin !== undefined) { + host = new URL(origin).host + } + return host !== undefined ? brandings[host] : null +} diff --git a/services/sign/pod-sign/src/client.ts b/services/sign/pod-sign/src/client.ts new file mode 100644 index 0000000000..156f017d28 --- /dev/null +++ b/services/sign/pod-sign/src/client.ts @@ -0,0 +1,24 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Client } from '@hcengineering/core' +import { getTransactorEndpoint } from '@hcengineering/server-client' +import { createClient } from '@hcengineering/server-client/src' + +export async function getClient (token: string): Promise { + const endpoint = await getTransactorEndpoint(token) + return await createClient(endpoint, token) +} diff --git a/services/sign/pod-sign/src/config.ts b/services/sign/pod-sign/src/config.ts new file mode 100644 index 0000000000..3d8c0c6613 --- /dev/null +++ b/services/sign/pod-sign/src/config.ts @@ -0,0 +1,43 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import fs from 'fs' + +export interface Config { + AccountsUrl: string + Cert: Buffer + CertPwd: string + DbURL: string + Port: number + Secret: string + ServiceID: string + SystemEmail: string + BrandingPath: string +} + +const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined) + +const config: Config = (() => { + const params: Partial = { + AccountsUrl: process.env.ACCOUNTS_URL, + Cert: process.env.CERTIFICATE_PATH !== undefined ? fs.readFileSync(process.env.CERTIFICATE_PATH) : undefined, + CertPwd: process.env.CERTIFICATE_PASSWORD ?? '', + DbURL: process.env.MONGO_URL, + Port: parseNumber(process.env.PORT) ?? 4006, + Secret: process.env.SECRET, + ServiceID: process.env.SERVICE_ID, + SystemEmail: process.env.SYSTEM_EMAIL ?? 'anticrm@hc.engineering', + BrandingPath: process.env.BRANDING_PATH ?? '' + } + + const missingEnv = (Object.keys(params) as Array).filter((key) => params[key] === undefined) + + if (missingEnv.length > 0) { + throw Error(`Missing config for attributes: ${missingEnv.join(', ')}`) + } + + return params as Config +})() + +export default config diff --git a/services/sign/pod-sign/src/error.ts b/services/sign/pod-sign/src/error.ts new file mode 100644 index 0000000000..dc17f767c5 --- /dev/null +++ b/services/sign/pod-sign/src/error.ts @@ -0,0 +1,40 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +const generateMessage = (code: number): string => { + if (code === 401) { + return 'Unauthorized' + } + + if (code === 404) { + return 'Not Found' + } + + if (code === 400) { + return 'Bad Request' + } + + return 'Error' +} + +export class ApiError extends Error { + readonly code: number + + constructor (code: number, message?: string) { + super(message ?? generateMessage(code)) + this.code = code + } +} diff --git a/services/sign/pod-sign/src/index.ts b/services/sign/pod-sign/src/index.ts new file mode 100644 index 0000000000..e18b8ddb2a --- /dev/null +++ b/services/sign/pod-sign/src/index.ts @@ -0,0 +1,21 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { config } from 'dotenv' +import { main } from './main' + +config() +void main() diff --git a/services/sign/pod-sign/src/main.ts b/services/sign/pod-sign/src/main.ts new file mode 100644 index 0000000000..3ff6369249 --- /dev/null +++ b/services/sign/pod-sign/src/main.ts @@ -0,0 +1,37 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// + +import { setMetadata } from '@hcengineering/platform' +import { storageConfigFromEnv } from '@hcengineering/server' +import serverClient from '@hcengineering/server-client' +import { loadBrandingMap } from '@hcengineering/server-core' +import serverToken from '@hcengineering/server-token' + +import config from './config' +import { createServer, listen } from './server' + +const setupMetadata = (): void => { + setMetadata(serverToken.metadata.Secret, config.Secret) + setMetadata(serverClient.metadata.Endpoint, config.AccountsUrl) + setMetadata(serverClient.metadata.UserAgent, config.ServiceID) +} + +export const main = async (): Promise => { + setupMetadata() + const storageConfig = storageConfigFromEnv() + const server = listen(createServer(config.DbURL, storageConfig, loadBrandingMap(config.BrandingPath)), config.Port) + + const shutdown = (): void => { + server.close(() => process.exit()) + } + + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + process.on('uncaughtException', (e) => { + console.error(e) + }) + process.on('unhandledRejection', (e) => { + console.error(e) + }) +} diff --git a/services/sign/pod-sign/src/server.ts b/services/sign/pod-sign/src/server.ts new file mode 100644 index 0000000000..1e910e0f65 --- /dev/null +++ b/services/sign/pod-sign/src/server.ts @@ -0,0 +1,116 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import cors from 'cors' +import express, { type Express, type NextFunction, type Request, type Response } from 'express' +import { Token } from '@hcengineering/server-token' +import { type Server } from 'http' +import { StorageConfiguration } from '@hcengineering/server-core' +import { buildStorageFromConfig } from '@hcengineering/server' +import { MeasureMetricsContext, generateId } from '@hcengineering/core' + +import { ApiError } from './error' +import { signPDF } from './sign' +import { extractToken } from './token' +import { type Branding, type BrandingMap, extractBranding } from './branding' +import config from './config' + +type AsyncRequestHandler = ( + req: Request, + res: Response, + token: Token, + branding: Branding | null, + next: NextFunction +) => Promise + +const handleRequest = async ( + fn: AsyncRequestHandler, + brandings: BrandingMap, + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const { token } = extractToken(req.headers, req.query) + const branding = extractBranding(brandings, req.headers) + await fn(req, res, token, branding, next) + } catch (err: unknown) { + next(err) + } +} + +const wrapRequest = + (brandings: BrandingMap, fn: AsyncRequestHandler) => (req: Request, res: Response, next: NextFunction) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + handleRequest(fn, brandings, req, res, next) + } + +export function createServer (dbUrl: string, storageConfig: StorageConfiguration, brandings: BrandingMap): Express { + const storageAdapter = buildStorageFromConfig(storageConfig, dbUrl) + const measureCtx = new MeasureMetricsContext('sign', {}) + + const app = express() + app.use(cors()) + app.use(express.json()) + + app.post( + '/sign', + wrapRequest(brandings, async (req, res, token, branding) => { + const fileId = req.body.fileId as string + + if (fileId === undefined) { + throw new ApiError(400, 'Missing fileId') + } + + const originalFile = await storageAdapter.read(measureCtx, token.workspace, fileId) + const ctx = { + title: branding?.title ?? 'Huly' + } + const signRes = await signPDF(Buffer.concat(originalFile), config.Cert, config.CertPwd, ctx) + + if (signRes === undefined) { + throw new ApiError(400, 'Failed to sign') + } + + const signedId = `signed-${fileId}-${generateId()}` + + await storageAdapter.put(measureCtx, token.workspace, signedId, signRes, 'application/pdf', signRes.length) + + res.contentType('application/json') + res.send({ id: signedId }) + }) + ) + + app.use((err: any, _req: any, res: any, _next: any) => { + console.log(err) + if (err instanceof ApiError) { + res.status(err.code).send({ code: err.code, message: err.message }) + return + } + + res.status(500).send(err.message?.length > 0 ? { message: err.message } : err) + }) + + return app +} + +export function listen (e: Express, port: number, host?: string): Server { + const cb = (): void => { + console.log(`Sign service has been started at ${host ?? '*'}:${port}`) + } + + return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb) +} diff --git a/services/sign/pod-sign/src/sign.ts b/services/sign/pod-sign/src/sign.ts new file mode 100644 index 0000000000..a0dab86df4 --- /dev/null +++ b/services/sign/pod-sign/src/sign.ts @@ -0,0 +1,128 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +import { pdflibAddPlaceholder } from '@signpdf/placeholder-pdf-lib' +import { P12Signer } from '@signpdf/signer-p12' +import signpdf from '@signpdf/signpdf' +import { PDFDocument, StandardFonts, degrees, degreesToRadians, rgb } from 'pdf-lib' + +import config from './config' + +interface Rect { + x: number + y: number + width: number + height: number + options?: { + rotate?: number + } +} + +interface Options { + name: string + contactInfo: string + appName: string + reason: string + location: string +} + +interface Context { + title: string +} + +/** + * Signs a PDF document. + * @public + * @returns Buffer with the signed content. + */ +export async function signPDF (file: Buffer, certp12: Buffer, pwd: string, ctx: Context): Promise { + const rect: Rect = { + x: 50, + y: 70, + width: 200, + height: 40, + options: { + rotate: -3 // Currently only supports [-90;0] range for the sake of simplicity + } + } + + // Hardcoded options for signing on export. + // Make it configurable when will be needed to allow signing for different reasons. + const options: Options = { + name: ctx.title, + contactInfo: config.SystemEmail, + appName: ctx.title, + reason: 'Export from the system', + location: 'N/A' + } + // Just display some information that will become a visible part of the signature. + // It has nothing to do with the signature itself, it's just placed on top of the + // signature widget which will mean clicking on this content will pop up the info + // about the signature and the ceritifcate in applications that allow verifying PDF signatures. + const pdfDoc = await drawManifestation(file, options, rect) + + const deg = rect.options?.rotate ?? 0 + const shiftRate = -Math.sin(degreesToRadians(deg)) + const shiftWidth = rect.height * shiftRate + const shiftHeight = rect.width * shiftRate + + // The PDF needs to have a placeholder for a signature to be added. + pdflibAddPlaceholder({ + pdfDoc, + widgetRect: [rect.x, rect.y - shiftHeight, rect.x + rect.width + shiftWidth, rect.y + rect.height], + ...options + }) + + const preparedDoc = await pdfDoc.save() + const signer = new P12Signer(certp12, { passphrase: pwd }) + + return await signpdf.sign(preparedDoc, signer) +} + +async function drawManifestation (file: Buffer, options: Options, rect: Rect): Promise { + const pdfDoc = await PDFDocument.load(file) + + // Embed the Helvetica font + const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica) + const color = rgb(77 / 255, 176 / 255, 224 / 255) // some blue stamp color emulating wet inc + const deg = rect.options?.rotate ?? 0 + const rotate = degrees(deg) + + // Get the first page of the document + const pages = pdfDoc.getPages() + const firstPage = pages[0] + + firstPage.drawRectangle({ + ...rect, + borderWidth: 0.5, + borderColor: color, + rotate + }) + + // Include options + const x = rect.x + 8 + let y = rect.y + rect.height - 16 + const size = 11 + const lineHeight = 14 + const font = helveticaFont + + const lines = [`Exported from ${options.appName}`, `Date: ${new Date().toISOString()}`] + + const shiftRate = -Math.sin(degreesToRadians(deg)) + + lines.forEach((line) => { + firstPage.drawText(line, { + x: x + (y - rect.y) * shiftRate, + y, + size, + font, + color, + lineHeight, + maxWidth: rect.width - 10, + rotate + }) + y -= lineHeight + }) + + return pdfDoc +} diff --git a/services/sign/pod-sign/src/signController.ts b/services/sign/pod-sign/src/signController.ts new file mode 100644 index 0000000000..5d627a516a --- /dev/null +++ b/services/sign/pod-sign/src/signController.ts @@ -0,0 +1,74 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type Client } from '@hcengineering/core' +import { generateToken, type Token } from '@hcengineering/server-token' + +import { createClient, getTransactorEndpoint } from '@hcengineering/server-client' +import config from './config' + +export class SignController { + private readonly clients: Map = new Map() + + protected static _instance: SignController + + private constructor () { + SignController._instance = this + } + + static getSignController (): SignController { + if (SignController._instance !== undefined) { + return SignController._instance + } + + return new SignController() + } + + async getClient (token: Token): Promise { + const { workspace } = token + const clientKey = this.getClientKey(workspace.name) + let client = this.clients.get(clientKey) + + if (client === undefined) { + client = await this.createPlatformClient(workspace.name) + this.clients.set(clientKey, client) + } + + return client + } + + private async createPlatformClient (workspace: string): Promise { + const token = generateToken(config.SystemEmail, { + name: workspace, + productId: '' + }) + const endpoint = await getTransactorEndpoint(token) + const connection = await createClient(token, endpoint) + + console.log(`Platform client has been created (workspace: ${workspace})`) + + return connection + } + + private getClientKey (workspace: string): string { + return `${workspace}` + } + + cleanUp (): void { + for (const client of this.clients.values()) { + void client.close() + } + } +} diff --git a/services/sign/pod-sign/src/token.ts b/services/sign/pod-sign/src/token.ts new file mode 100644 index 0000000000..aeacac7cba --- /dev/null +++ b/services/sign/pod-sign/src/token.ts @@ -0,0 +1,72 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { IncomingHttpHeaders } from 'http' +import { decodeToken, Token } from '@hcengineering/server-token' + +import { ApiError } from './error' + +const extractRawCookieToken = (cookie?: string): string | null => { + if (cookie === undefined || cookie === null) { + return null + } + + const cookies = cookie.split(';') + const tokenCookie = cookies.find((cookie) => cookie.toLocaleLowerCase().includes('token')) + if (tokenCookie === undefined) { + return null + } + + return tokenCookie.split('=')[1] +} + +const extractRawAuthorizationToken = (authorization?: string): string | null => { + if (authorization === undefined || authorization === null) { + return null + } + + return authorization.split(' ')[1] +} + +const extractRawQueryToken = (queryParams: any): string | null => { + if (queryParams == null) { + return null + } + + return queryParams.token +} + +export const extractToken = (headers: IncomingHttpHeaders, queryParams: any): { token: Token, rawToken: string } => { + try { + const rawToken = + extractRawCookieToken(headers.cookie) ?? + extractRawAuthorizationToken(headers.authorization) ?? + extractRawQueryToken(queryParams) + + if (rawToken === null) { + throw new ApiError(401) + } + + const token = decodeToken(rawToken) + + if (token === null) { + throw new ApiError(401) + } + + return { token, rawToken } + } catch { + throw new ApiError(401) + } +} diff --git a/services/sign/pod-sign/tsconfig.json b/services/sign/pod-sign/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/sign/pod-sign/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/services/telegram/pod-telegram/.eslintrc.js b/services/telegram/pod-telegram/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/services/telegram/pod-telegram/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/telegram/pod-telegram/.npmignore b/services/telegram/pod-telegram/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/services/telegram/pod-telegram/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/telegram/pod-telegram/Dockerfile b/services/telegram/pod-telegram/Dockerfile new file mode 100644 index 0000000000..6134ee0ea6 --- /dev/null +++ b/services/telegram/pod-telegram/Dockerfile @@ -0,0 +1,9 @@ + +FROM node:20-alpine + +WORKDIR /usr/src/app + +COPY bundle/bundle.js ./ + +EXPOSE 8086 +CMD [ "node", "bundle.js" ] diff --git a/services/telegram/pod-telegram/config/rig.json b/services/telegram/pod-telegram/config/rig.json new file mode 100644 index 0000000000..0110930f55 --- /dev/null +++ b/services/telegram/pod-telegram/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/telegram/pod-telegram/jest.config.js b/services/telegram/pod-telegram/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/services/telegram/pod-telegram/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/telegram/pod-telegram/package.json b/services/telegram/pod-telegram/package.json new file mode 100644 index 0000000000..c694f3ffad --- /dev/null +++ b/services/telegram/pod-telegram/package.json @@ -0,0 +1,80 @@ +{ + "name": "@hcengineering/pod-telegram", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "_phase:bundle": "rushx bundle", + "_phase:docker-build": "rushx docker:build", + "_phase:docker-staging": "rushx docker:staging", + "bundle": "mkdir -p bundle && esbuild src/index.ts --bundle --platform=node > bundle/bundle.js", + "docker:build": "../../../common/scripts/docker_build.sh hardcoreeng/telegram", + "docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/telegram staging", + "docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/telegram", + "run-local": "cross-env ts-node src/index.ts", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@tsconfig/node16": "^1.0.4", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/mime": "^3.0.1", + "@types/node": "~20.11.16", + "@types/node-fetch": "~2.6.2", + "@types/ws": "^8.5.11", + "esbuild": "^0.20.0", + "prettier": "^3.1.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "ts-node-dev": "^2.0.0", + "ts-standard": "^12.0.2", + "bson": "^6.8.0", + "typescript": "^5.3.3", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "eslint": "^8.54.0" + }, + "dependencies": { + "@hcengineering/attachment": "^0.6.14", + "@hcengineering/client": "^0.6.18", + "@hcengineering/client-resources": "^0.6.27", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/core": "^0.6.32", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/setting": "^0.6.17", + "@hcengineering/telegram": "^0.6.21", + "@hcengineering/server-client": "^0.6.0", + "big-integer": "^1.6.51", + "dotenv": "~16.0.0", + "cors": "^2.8.5", + "express": "^4.19.2", + "form-data": "^4.0.0", + "htmlparser2": "^9.0.0", + "jwt-simple": "^0.5.6", + "mime": "^3.0.0", + "mongodb": "^6.8.0", + "node-fetch": "^2.6.6", + "telegram": "2.22.2", + "ws": "^8.18.0" + } +} diff --git a/services/telegram/pod-telegram/src/config.ts b/services/telegram/pod-telegram/src/config.ts new file mode 100644 index 0000000000..9f82078df2 --- /dev/null +++ b/services/telegram/pod-telegram/src/config.ts @@ -0,0 +1,103 @@ +interface Config { + Host: string | undefined + Port: number + + TelegramApiID: number + TelegramApiHash: string + TelegramAuthTTL: number + + MongoURI: string + MongoDB: string + + UploadUrl: string + AccountsURL: string + ServiceID: string + Secret: string + SystemEmail: string +} + +const envMap: { [key in keyof Config]: string } = { + Host: 'HOST', + Port: 'PORT', + + TelegramApiID: 'TELEGRAM_API_ID', + TelegramApiHash: 'TELEGRAM_API_HASH', + TelegramAuthTTL: 'TELEGRAM_AUTH_TTL', + + MongoURI: 'MONGO_URI', + MongoDB: 'MONGO_DB', + + UploadUrl: 'UPLOAD_URL', + AccountsURL: 'ACCOUNTS_URL', + ServiceID: 'SERVICE_ID', + Secret: 'SECRET', + SystemEmail: 'SYSTEM_EMAIL' +} + +const defaults: Partial = { + Host: undefined, + Port: 8086, + + TelegramApiID: undefined, + TelegramApiHash: undefined, + TelegramAuthTTL: 600 * 1000, + + MongoURI: undefined, + MongoDB: 'telegram-service', + + AccountsURL: undefined, + ServiceID: 'telegram-service', + SystemEmail: 'anticrm@hc.engineering', + + Secret: undefined +} + +const required: Array = [ + 'TelegramApiID', + 'TelegramApiHash', + 'UploadUrl', + 'MongoURI', + 'AccountsURL', + 'Secret' +] + +const mergeConfigs = (defaults: Partial, params: Partial): T => { + const result = { ...defaults } + for (const key in params) { + if (params[key] !== undefined) { + result[key] = params[key] + } + } + return result as T +} + +const parseNumber = (str: string | undefined): number | undefined => (str != null ? Number(str) : undefined) + +const config = (() => { + const ttl = parseNumber(process.env[envMap.TelegramAuthTTL]) + const params: Partial = { + Host: process.env[envMap.Host], + Port: parseNumber(process.env[envMap.Port]), + TelegramApiID: parseNumber(process.env[envMap.TelegramApiID]), + TelegramApiHash: process.env[envMap.TelegramApiHash], + TelegramAuthTTL: ttl === undefined ? ttl : ttl * 1000, + UploadUrl: process.env[envMap.UploadUrl], + MongoDB: process.env[envMap.MongoDB], + MongoURI: process.env[envMap.MongoURI], + AccountsURL: process.env[envMap.AccountsURL], + ServiceID: process.env[envMap.ServiceID], + SystemEmail: process.env[envMap.SystemEmail], + Secret: process.env[envMap.Secret] + } + + const missingEnv = required.filter((key) => params[key] === undefined).map((key) => envMap[key]) + + if (missingEnv.length > 0) { + throw Error(`Missing env variables: ${missingEnv.join(', ')}`) + } + + const res = mergeConfigs(defaults, params) + return res +})() + +export default config diff --git a/services/telegram/pod-telegram/src/error.ts b/services/telegram/pod-telegram/src/error.ts new file mode 100644 index 0000000000..e24bbe0283 --- /dev/null +++ b/services/telegram/pod-telegram/src/error.ts @@ -0,0 +1,12 @@ +export class ApiError extends Error { + constructor ( + readonly code: string, + readonly message: string + ) { + super(message) + } +} + +export enum Code { + PhoneCodeInvalid = 'PHONE_CODE_INVALID' +} diff --git a/services/telegram/pod-telegram/src/escaping.ts b/services/telegram/pod-telegram/src/escaping.ts new file mode 100644 index 0000000000..af97c98308 --- /dev/null +++ b/services/telegram/pod-telegram/src/escaping.ts @@ -0,0 +1,10 @@ +export const escape = (str: string): string => { + try { + return str.replace(/&/g, '&').replace(//g, '>') + } catch (ee: any) { + console.error(ee) + } + return str +} + +export const unescape = (str: string): string => str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') diff --git a/services/telegram/pod-telegram/src/index.ts b/services/telegram/pod-telegram/src/index.ts new file mode 100644 index 0000000000..02db513f75 --- /dev/null +++ b/services/telegram/pod-telegram/src/index.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line +require('dotenv').config() +// eslint-disable-next-line +import { main } from './main' + +void main() diff --git a/services/telegram/pod-telegram/src/jwt.ts b/services/telegram/pod-telegram/src/jwt.ts new file mode 100644 index 0000000000..d03a5c8c6f --- /dev/null +++ b/services/telegram/pod-telegram/src/jwt.ts @@ -0,0 +1,6 @@ +import { encode as jwtEncode, decode as jwtDecode } from 'jwt-simple' + +import config from './config' + +export const encode = (data: any): string => jwtEncode(data, config.Secret) +export const decode = (data: string): any => jwtDecode(data, config.Secret) diff --git a/services/telegram/pod-telegram/src/main.ts b/services/telegram/pod-telegram/src/main.ts new file mode 100644 index 0000000000..287d477102 --- /dev/null +++ b/services/telegram/pod-telegram/src/main.ts @@ -0,0 +1,203 @@ +import { IncomingHttpHeaders } from 'http' +import { decode } from './jwt' +import { PlatformWorker } from './platform' +import { createServer, Handler, listen } from './server' +import { telegram } from './telegram' + +import { setMetadata } from '@hcengineering/platform' +import serverClient from '@hcengineering/server-client' +import config from './config' + +const extractToken = (header: IncomingHttpHeaders): any => { + try { + return decode(header.authorization?.slice(7) ?? '') + } catch { + return undefined + } +} + +export const main = async (): Promise => { + setMetadata(serverClient.metadata.Endpoint, config.AccountsURL) + setMetadata(serverClient.metadata.UserAgent, config.ServiceID) + + const platformWorker = await PlatformWorker.create() + const endpoints: Array<[string, Handler]> = [ + [ + '/signin', + async (req, res) => { + const token = extractToken(req.headers) + + if (token === undefined) { + res.status(401).send() + return + } + + const { email, workspace } = token + const phone = req.body?.phone + if (phone === undefined) { + res.status(400).send({ err: "'phone' is missing" }) + return + } + + const existingRec = await platformWorker.getUserRecord({ + phone, + workspace + }) + + if (existingRec !== undefined) { + if (existingRec.email === email) { + res.send({ + next: 'end' + }) + } else { + res.status(400).send({ err: 'Phone number already in use' }) + } + + return + } + + const next = await telegram.auth(phone) + res.send({ next }) + } + ], + [ + '/signin/code', + async (req, res) => { + const token = extractToken(req.headers) + + if (token === undefined) { + res.status(401).send() + return + } + + const { email, workspace } = token + const phone = req.body?.phone + if (phone === undefined) { + res.status(400).send({ err: "'phone' is missing" }) + return + } + + const existingRec = await platformWorker.getUserRecord({ + phone, + workspace + }) + + if (existingRec !== undefined) { + if (existingRec.email === email) { + res.send({ + next: 'end' + }) + } else { + res.status(400).send({ err: 'Phone number already in use' }) + } + + return + } + + const done = await telegram.authCode(phone, req.body.code ?? '') + + if (done) { + const conn = telegram.getConnection(phone) + + if (conn !== undefined) { + await platformWorker.addUser({ + email, + workspace, + phone, + conn + }) + telegram.forgetConnection(phone) + } + } + + res.send({ + next: done ? 'end' : 'pass' + }) + } + ], + [ + '/signin/pass', + async (req, res) => { + const token = extractToken(req.headers) + + if (token === undefined) { + res.status(401).send() + return + } + + const { email, workspace } = token + const phone = req.body?.phone + if (phone === undefined) { + res.status(400).send({ err: "'phone' is missing" }) + return + } + + const existingRec = await platformWorker.getUserRecord({ + phone, + workspace + }) + + if (existingRec !== undefined) { + if (existingRec.email === email) { + res.send({ + next: 'end' + }) + } else { + res.status(400).send({ err: 'Phone number already in use' }) + } + + return + } + + await telegram.authPass(phone, req.body.pass ?? '') + + const conn = telegram.getConnection(phone) + + if (conn !== undefined) { + await platformWorker.addUser({ + email, + workspace, + phone, + conn + }) + telegram.forgetConnection(phone) + } + + res.send({ next: 'end' }) + } + ], + [ + '/signout', + async (req, res) => { + const token = extractToken(req.headers) + + if (token === undefined) { + res.status(401).send() + return + } + + const { email, workspace } = token + await platformWorker.removeUser({ email, workspace }) + + res.send() + } + ] + ] + + const server = listen(createServer(endpoints), config.Port, config.Host) + + const shutdown = (): void => { + server.close() + + void Promise.all([platformWorker.close()]).then(() => process.exit()) + } + + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + process.on('uncaughtException', (e) => { + console.error(e) + }) + process.on('unhandledRejection', (e) => { + console.error(e) + }) +} diff --git a/services/telegram/pod-telegram/src/markup.ts b/services/telegram/pod-telegram/src/markup.ts new file mode 100644 index 0000000000..f89137eee1 --- /dev/null +++ b/services/telegram/pod-telegram/src/markup.ts @@ -0,0 +1,192 @@ +import { Api } from 'telegram' +import { Parser } from 'htmlparser2' + +import { escape, unescape } from './escaping' + +type TelegramMessage = Pick +interface BaseProps { + length: number + offset: number + url?: string +} +type EntityBuilder = (props: BaseProps) => Api.TypeMessageEntity +const entityMap = new Map([ + ['strong', (props) => new Api.MessageEntityBold(props)], + ['em', (props) => new Api.MessageEntityItalic(props)], + ['s', (props) => new Api.MessageEntityStrike(props)], + ['blockquote', (props) => new Api.MessageEntityBlockquote(props)], + ['code', (props) => new Api.MessageEntityCode(props)], + ['a', (props) => new Api.MessageEntityTextUrl(props as any)] +]) + +export function platformToTelegram (message: string): TelegramMessage { + const openedTags = new Map< + string, + { + offset: number + count: number + url?: string + } + >() + let pureMessage = '' + const entities: Api.TypeMessageEntity[] = [] + const parser = new Parser({ + onopentag: (tag, attrs) => { + if (tag === 'br' || tag === 'p') { + return + } + + const existingTag = openedTags.get(tag) + if (existingTag !== undefined) { + existingTag.count += 1 + return + } + + // Just skip unsupported tag + if (!entityMap.has(tag)) { + return + } + + openedTags.set(tag, { + count: 1, + offset: pureMessage.length, + url: attrs.href + }) + }, + ontext: (text) => { + pureMessage += unescape(text) + }, + onclosetag: (tag) => { + if (tag === 'br') { + pureMessage += '\n' + return + } + + if (tag === 'p') { + pureMessage += '\n\n' + return + } + + const existingTag = openedTags.get(tag) + + // We have unknown tag + if (existingTag === undefined) { + return + } + + existingTag.count -= 1 + if (existingTag.count > 0) { + return + } + + openedTags.delete(tag) + + const entityBuilder = entityMap.get(tag) + if (entityBuilder === undefined) { + return + } + + const offset = existingTag.offset + const length = pureMessage.length - offset + entities.push(entityBuilder({ offset, length, url: existingTag.url })) + } + }) + + parser.write(message) + parser.end() + + const result = pureMessage.trim() + + // Adjust entities rely on trimmed chars + entities.forEach((e) => { + e.length = Math.min(e.length, result.length - e.offset) + }) + + return { + message: result, + entities + } +} + +const transform = (text: string): string => escape(text).replace(/\n/g, '
') + +// FYI, copypasted from here: +// https://github.com/gram-js/gramjs/blob/master/gramjs/extensions/html.ts +// https://github.com/LonamiWebs/Telethon/blob/master/telethon/extensions/html.py +// with multiple fixes and platform adjustements applied +export function telegramToPlatform ( + { message: text, entities }: TelegramMessage, + _offset = 0, + _length?: number +): string { + // WARNING: It seems message can be undefined even it is not reflected in types + if (text == null || text === '') { + return '' + } + + if (entities == null || entities.length === 0) { + return transform(text) + } + + if (_length === undefined) { + _length = text.length + } + + const html = [] + let lastOffset = 0 + for (let i = 0; i < entities.length; i++) { + const entity = entities[i] + if (entity.offset >= _offset + _length) { + break + } + + const relativeOffset = entity.offset - _offset + if (relativeOffset > lastOffset) { + html.push(transform(text.slice(lastOffset, relativeOffset))) + } else if (relativeOffset < lastOffset) { + continue + } + + let skipEntity = false + const length = entity.length + const entityText = telegramToPlatform( + { + message: text.slice(relativeOffset, relativeOffset + length), + entities: entities.slice(i + 1, entities.length) + }, + entity.offset, + length + ) + if (entity instanceof Api.MessageEntityBold) { + html.push(`${entityText}`) + } else if (entity instanceof Api.MessageEntityItalic) { + html.push(`${entityText}`) + } else if (entity instanceof Api.MessageEntityBold) { + html.push(`${entityText}`) + } else if (entity instanceof Api.MessageEntityCode) { + html.push(`${entityText}`) + } else if (entity instanceof Api.MessageEntityUnderline) { + html.push(`${entityText}`) + } else if (entity instanceof Api.MessageEntityStrike) { + html.push(`${entityText}`) + } else if (entity instanceof Api.MessageEntityBlockquote) { + html.push(`
${entityText}
`) + } else if (entity instanceof Api.MessageEntityPre) { + html.push(`${entityText}`) + } else if (entity instanceof Api.MessageEntityEmail) { + html.push(entityText) + } else if (entity instanceof Api.MessageEntityUrl) { + html.push(entityText) + } else if (entity instanceof Api.MessageEntityTextUrl) { + html.push(`${entityText}`) + } else if (entity instanceof Api.MessageEntityMentionName) { + html.push(entityText) + } else { + skipEntity = true + } + lastOffset = relativeOffset + (skipEntity ? 0 : length) + } + + html.push(transform(text.slice(lastOffset, text.length))) + return html.join('') +} diff --git a/services/telegram/pod-telegram/src/platform.ts b/services/telegram/pod-telegram/src/platform.ts new file mode 100644 index 0000000000..dd3717871a --- /dev/null +++ b/services/telegram/pod-telegram/src/platform.ts @@ -0,0 +1,104 @@ +import type { Collection } from 'mongodb' +import { getDB } from './storage' +import { LastMsgRecord, TgUser, User, UserRecord, WorkspaceChannel } from './types' +import { WorkspaceWorker } from './workspace' + +export class PlatformWorker { + private constructor ( + private readonly clientMap: Map, + private readonly storage: Collection + ) {} + + async close (): Promise { + await Promise.all( + [...this.clientMap.values()].map(async (worker) => { + await worker.close() + }) + ) + } + + async addUser (tgUser: TgUser): Promise { + const { workspace, phone } = tgUser + const res = await this.storage.findOne({ phone, workspace }) + + if (res !== null) { + throw Error('Phone number is already used') + } + + let wsWorker = this.clientMap.get(workspace) + + if (wsWorker === undefined) { + const [userStorage, lastMsgStorage, channelStorage] = await PlatformWorker.createStorages() + wsWorker = await WorkspaceWorker.create(workspace, userStorage, lastMsgStorage, channelStorage) + this.clientMap.set(workspace, wsWorker) + } + + await wsWorker.addUser(tgUser) + } + + async getTarget ({ workspace, email }: User): Promise<[UserRecord, WorkspaceWorker | undefined]> { + const res = await this.storage.findOne({ email, workspace }) + + if (res === null) { + throw Error('User is not signed in') + } + + return [res, this.clientMap.get(workspace)] + } + + async removeUser (user: User): Promise { + const [res, wsWorker] = await this.getTarget(user) + + if (wsWorker === undefined) { + throw Error(`Invalid workspace: '${user.workspace}'`) + } + + await wsWorker.removeUser({ phone: res.phone }) + } + + async getUserRecord ({ workspace, phone }: Pick): Promise { + return (await this.storage.findOne({ phone, workspace })) ?? undefined + } + + static async createStorages (): Promise< + [Collection, Collection, Collection] + > { + const db = await getDB() + const userStorage = db.collection('integrations') + const lastMsgStorage = db.collection('last-msgs') + const channelStorage = db.collection('channels') + + await userStorage.createIndex({ phone: 1, workspace: 1 }, { unique: true }) + await userStorage.createIndex({ email: 1, workspace: 1 }, { unique: true }) + + try { + await lastMsgStorage.dropIndex('phone_1_participantID_1_workspace_1') + } catch {} + + await lastMsgStorage.createIndex({ phone: 1, participantID: 1, channelID: 1, workspace: 1 }, { unique: true }) + + return [userStorage, lastMsgStorage, channelStorage] + } + + static async create (): Promise { + const [userStorage, lastMsgStorage, channelStorage] = await PlatformWorker.createStorages() + const workspaces = new Set((await userStorage.find().toArray()).map((p) => p.workspace)) + const clients: Array<[string, WorkspaceWorker]> = [] + for (const workspace of workspaces) { + try { + const worker = await WorkspaceWorker.create(workspace, userStorage, lastMsgStorage, channelStorage) + clients.push([workspace, worker]) + void worker.checkUsers() + } catch (e) { + console.error(`Failed to initialize workspace worker: ${workspace}`) + console.error(e) + } + } + + const res = clients.filter((client): client is [string, WorkspaceWorker] => client !== undefined) + + const worker = new PlatformWorker(new Map(res), userStorage) + + return worker + } +} diff --git a/services/telegram/pod-telegram/src/queue.ts b/services/telegram/pod-telegram/src/queue.ts new file mode 100644 index 0000000000..67df5fabcb --- /dev/null +++ b/services/telegram/pod-telegram/src/queue.ts @@ -0,0 +1,45 @@ +import { Event } from './types' + +export class MsgQueue { + private events: Event[] = [] + private inProgress = false + + constructor ( + private paused: boolean, + private readonly onEvent: (event: Event) => Promise + ) {} + + add (event: Event): void { + this.events.push(event) + + void this.handle() + } + + dropBefore (id: number): void { + this.events = this.events.filter((x) => x.msg.id > id) + } + + unpause (): void { + this.paused = false + + void this.handle() + } + + pause (): void { + this.paused = true + } + + private async handle (): Promise { + while (!this.paused) { + if (this.inProgress) continue + const event = this.events.shift() + + if (event === undefined) { + return + } + this.inProgress = true + await this.onEvent(event) + this.inProgress = false + } + } +} diff --git a/services/telegram/pod-telegram/src/server.ts b/services/telegram/pod-telegram/src/server.ts new file mode 100644 index 0000000000..66f2358862 --- /dev/null +++ b/services/telegram/pod-telegram/src/server.ts @@ -0,0 +1,50 @@ +import express, { NextFunction, Request, Response, Express } from 'express' +import cors from 'cors' +import { Server } from 'http' +import { ApiError } from './error' + +export type Handler = (req: Request, res: Response, next?: NextFunction) => Promise + +const catchError = (fn: Handler) => (req: Request, res: Response, next: NextFunction) => { + void (async () => { + try { + await fn(req, res, next) + } catch (err: unknown) { + next(err) + } + })() +} + +export function createServer (endpoints: Array<[string, Handler]>): Express { + const app = express() + + app.use(cors()) + app.use(express.json()) + + endpoints.forEach(([endpoint, handler]) => { + app.post(endpoint, catchError(handler)) + }) + + app.use((_req, res, _next) => { + res.status(404).send({ message: 'Not found' }) + }) + + app.use((err: any, _req: any, res: any, _next: any) => { + if (err instanceof ApiError) { + res.status(400).send({ code: err.code, message: err.message }) + return + } + + res.status(500).send({ message: err.message }) + }) + + return app +} + +export function listen (e: Express, port: number, host?: string): Server { + const cb = (): void => { + console.log(`App has been started at ${host ?? '*'}:${port}`) + } + + return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb) +} diff --git a/services/telegram/pod-telegram/src/storage.ts b/services/telegram/pod-telegram/src/storage.ts new file mode 100644 index 0000000000..36e8745543 --- /dev/null +++ b/services/telegram/pod-telegram/src/storage.ts @@ -0,0 +1,16 @@ +import { MongoClient } from 'mongodb' + +import config from './config' + +export const getDB = (() => { + let client: MongoClient | undefined + + return async () => { + if (client === undefined) { + client = new MongoClient(config.MongoURI) + await client.connect() + } + + return client.db(config.MongoDB) + } +})() diff --git a/services/telegram/pod-telegram/src/telegram.ts b/services/telegram/pod-telegram/src/telegram.ts new file mode 100644 index 0000000000..34d9e96ebc --- /dev/null +++ b/services/telegram/pod-telegram/src/telegram.ts @@ -0,0 +1,417 @@ +import bigInt from 'big-integer' +import { Api, Logger, TelegramClient } from 'telegram' +import { computeCheck } from 'telegram/Password' +import type { _MessagesIter } from 'telegram/client/messages' +import type { EntityLike } from 'telegram/define' +import { RPCError } from 'telegram/errors' +import type { NewMessageEvent } from 'telegram/events' +import { NewMessage } from 'telegram/events' +import { StringSession } from 'telegram/sessions' +import type { Dialog } from 'telegram/tl/custom/dialog' +import config from './config' +import { ApiError, Code } from './error' + +Logger.setLevel('none') + +type SignInState = 'init' | 'code' | 'pass' | 'end' +class SignInFlow { + private _state: SignInState = 'init' + private codeHash: string | undefined + + constructor ( + private readonly client: TelegramClient, + private readonly phone: string, + private readonly onEnd: () => void + ) {} + + async init (): Promise { + if (this._state !== 'init') { + throw Error('Invalid sign in method') + } + + const res = await this.client.sendCode(this.creds, this.phone) + this.codeHash = res.phoneCodeHash + this._state = 'code' + } + + get state (): SignInState { + return this._state + } + + async code (code: string): Promise { + if (this._state !== 'code') { + throw Error('Invalid sign in method') + } + + try { + const res = await this.client.invoke( + new Api.auth.SignIn({ + phoneNumber: this.phone, + phoneCodeHash: this.codeHash, + phoneCode: code + }) + ) + + if (res instanceof Api.auth.AuthorizationSignUpRequired) { + throw Error('Account does not exist') + } + + this.finish() + return true + } catch (err: unknown) { + if (err instanceof RPCError) { + const untypedErr: any = err + if (untypedErr.errorMessage === 'SESSION_PASSWORD_NEEDED') { + this._state = 'pass' + return false + } + + if (untypedErr.errorMessage === 'PHONE_CODE_INVALID') { + throw new ApiError(Code.PhoneCodeInvalid, err.message) + } + } + + throw err + } + } + + async pass (password: string): Promise { + if (this._state !== 'pass') { + throw Error('Invalid sign in method') + } + + const passSrpRes = await this.client.invoke(new Api.account.GetPassword()) + const passSrpCheck = await computeCheck(passSrpRes, password) + await this.client.invoke(new Api.auth.CheckPassword({ password: passSrpCheck })) + + this.finish() + } + + private finish (): void { + this._state = 'end' + this.onEnd() + } + + private get creds (): { apiId: number, apiHash: string } { + return { + apiId: this.client.apiId, + apiHash: this.client.apiHash + } + } +} + +type Listener = (user: Api.User, msg: Api.Message) => void + +class TelegramConnection { + private _signInFlow: SignInFlow | undefined + private readonly listeners = new Map() + private subID = 0 + private handlerAdded = false + private readinessInterval: NodeJS.Timeout | undefined + private connecting = false + + constructor ( + readonly client: TelegramClient, + readonly phone: string + ) { + // Important: this routine is also required to keep getting telegram updates + // For some reason gramjs (or Telegram API) stops getting updates after minute or so and + // calling api methods works like workaround + this.setReadinessInterval() + } + + setReadinessInterval (): void { + if (this.readinessInterval === undefined) { + this.readinessInterval = setInterval(() => { + void this.reconnect() + }, 30 * 1000) + } + } + + async tryReconnect (): Promise { + if (this.connecting) { + return + } + + if (this.client.connected === true) { + console.log('Already connected') + return + } + + try { + this.connecting = true + await this.client.connect() + } catch (e: unknown) { + console.error(e) + } finally { + this.connecting = false + } + } + + async reconnect (): Promise { + try { + const ready = await this.isReady() + + if (!ready) { + await this.tryReconnect() + } + } catch (err) { + console.log(err) + } + } + + async close (): Promise { + if (this.readinessInterval !== undefined) { + clearInterval(this.readinessInterval) + } + this.client.listEventHandlers().forEach(([builder, callback]) => { + this.client.removeEventHandler(callback, builder) + }) + await this.client.disconnect() + } + + async signOut (): Promise { + await this.client.invoke(new Api.auth.LogOut()) + } + + async signIn (): Promise { + this._signInFlow = new SignInFlow(this.client, this.phone, () => { + this._signInFlow = undefined + this.client.session.save() + }) + + await this._signInFlow.init() + } + + async isReady (): Promise { + if (this._signInFlow !== undefined) { + return false + } + + return await this.client.isUserAuthorized() + } + + get signInFlow (): typeof this._signInFlow { + return this._signInFlow + } + + handleSub (): void { + if (this.handlerAdded) { + return + } + + this.handlerAdded = true + this.client.addEventHandler((update: NewMessageEvent) => { + void (async () => { + if (!(update.isPrivate ?? false)) { + return + } + + const user = await update.message.getChat() + if (user === undefined || user.className !== 'User') { + return + } + + for (const listener of this.listeners.values()) { + listener(user, update.message) + } + })() + }, new NewMessage({})) + } + + sub (l: Listener): () => void { + this.handleSub() + const subID = this.subID++ + this.listeners.set(subID, l) + + return () => this.listeners.delete(subID) + } + + getMsgs (user: EntityLike, from?: number, to?: number, limit?: number): _MessagesIter { + return this.client.iterMessages(user, { + minId: to, + limit, + maxId: from + }) as _MessagesIter + } + + async getUsers (): Promise { + const dialogs = this.client.iterDialogs({}) + const res: Api.User[] = [] + + for await (const dialog of dialogs) { + const tDialog: Dialog = dialog + if (tDialog.isUser && tDialog.entity?.className === 'User' && tDialog.entity.bot === false) { + res.push(tDialog.entity) + } + } + + return res + } + + async getUser (value: string): Promise { + try { + const res = await this.client.getEntity(value) + if (res.className === 'User' && res.bot === false) { + return res + } + } catch (e) { + console.log(e) + } + } + + async sendMsg ( + to: string, + message: string, + formattingEntities: Api.TypeMessageEntity[] = [], + file?: Buffer + ): Promise { + return await this.client.sendMessage(to, { + message, + formattingEntities, + file + }) + } + + // That means we want to write message to user by phone number + // when he is no in our contact list. That would lead an error. + async isContactImportRequired (to: string): Promise { + if (!to.startsWith('+')) { + return false + } + + const res = await this.client.invoke( + new Api.contacts.GetContacts({ + hash: bigInt(0) + }) + ) + + if (res.className === 'contacts.Contacts') { + const actualNumber = to.slice(1) + return !res.users.filter((x): x is Api.User => x.className === 'User').some((x) => x.phone === actualNumber) + } + + return true + } + + async addContact (contact: { phone: string, firstName: string, lastName: string }): Promise { + const result = await this.client.invoke( + new Api.contacts.ImportContacts({ + contacts: [ + new Api.InputPhoneContact({ + ...contact, + clientId: bigInt(0) + }) + ] + }) + ) + + if (result.imported.length < 1) { + throw Error('Failed to add contact') + } + } + + getToken (): string | undefined { + // TODO: Need recheck + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression + return (this.client.session.save() as never as string) ?? undefined + } +} + +export type TelegramConnectionInterface = InstanceType + +// Notice: not really elegant solution, for now have no better idea +// how to reorganize this mess. This helper works like buffer for connections +// that are not yet signed in and unified telegram connection creator. +export const telegram = new (class TelegramHelper { + readonly conns = new Map() + readonly ttls = new Map() + + async auth (phone: string): Promise { + const conn = await this.getOrCreate(phone) + + if (!this.ttls.has(phone)) { + this.ttls.set( + phone, + setTimeout(() => { + this.conns.delete(phone) + void conn.close() + }, config.TelegramAuthTTL) + ) + } + + if (conn.signInFlow !== undefined) { + return conn.signInFlow.state + } + + try { + await conn.signIn() + } catch (err) { + this.forgetConnection(phone) + await conn.close() + + throw err + } + + return 'code' + } + + async authCode (phone: string, code: string): Promise { + const conn = this.conns.get(phone) + + if (conn?.signInFlow === undefined) { + throw Error('Sign in is not initialized') + } + + return await conn.signInFlow.code(code) + } + + async authPass (phone: string, pass: string): Promise { + const conn = this.conns.get(phone) + + if (conn?.signInFlow === undefined) { + throw Error('Sign in is not initialized') + } + + await conn.signInFlow.pass(pass) + } + + async getOrCreate (phone: string): Promise { + const existingConn = this.conns.get(phone) + + if (existingConn !== undefined) { + return existingConn + } + + const conn = await this.create(phone) + this.conns.set(phone, conn) + + return conn + } + + getConnection (phone: string): TelegramConnection | undefined { + return this.conns.get(phone) + } + + forgetConnection (phone: string): void { + this.conns.delete(phone) + const timeout = this.ttls.get(phone) + + if (timeout !== undefined) { + this.ttls.delete(phone) + clearTimeout(timeout) + } + } + + async create (phone: string, token?: string): Promise { + const session = new StringSession(token) + const client = new TelegramClient(session, config.TelegramApiID, config.TelegramApiHash, { + connectionRetries: 5, + autoReconnect: true + }) + + await client.connect() + + return new TelegramConnection(client, phone) + } +})() diff --git a/services/telegram/pod-telegram/src/types.ts b/services/telegram/pod-telegram/src/types.ts new file mode 100644 index 0000000000..62ab8af83a --- /dev/null +++ b/services/telegram/pod-telegram/src/types.ts @@ -0,0 +1,64 @@ +import type { Document, ObjectId } from 'bson' +import { Api } from 'telegram' +import { TelegramConnectionInterface } from './telegram' +import { TelegramMessage as OldTelegramMessage } from '@hcengineering/telegram' +import { Timestamp } from '@hcengineering/core' + +export interface Doc extends Document { + _id?: ObjectId +} + +export interface UserRecord extends Doc { + phone: string + workspace: string + userId: string + email: string + token: string +} + +export interface LastMsgRecord extends Doc { + workspace: string + phone: string + participantID: string + channelID: string + maxMsgId: number + minMsgId: number +} + +export interface User { + email: string + workspace: string +} + +export interface TgUser extends User { + phone: string + conn: TelegramConnectionInterface +} + +export interface Contact { + firstName: string + lastName: string + phone: string +} + +export interface Event { + user: Api.User + msg: Api.Message +} + +export interface WorkspaceChannel { + workspace: string + value: string +} + +export interface AttachedFile { + size?: number + file: Buffer + type: string + lastModified: number + name: string +} + +export interface TelegramMessage extends OldTelegramMessage { + sendOn: Timestamp +} diff --git a/services/telegram/pod-telegram/src/utils.ts b/services/telegram/pod-telegram/src/utils.ts new file mode 100644 index 0000000000..52f4f41592 --- /dev/null +++ b/services/telegram/pod-telegram/src/utils.ts @@ -0,0 +1,164 @@ +import client, { ClientSocket } from '@hcengineering/client' +import { Client, Doc, Ref, Space } from '@hcengineering/core' +import { setMetadata } from '@hcengineering/platform' +import { createClient, getTransactorEndpoint } from '@hcengineering/server-client' +import FormData from 'form-data' +import mime from 'mime' +import fetch from 'node-fetch' +import { Api } from 'telegram' +import WebSocket from 'ws' +import config from './config' +import { AttachedFile } from './types' + +export class DeferredPromise { + public readonly promise: Promise + private _resolve: (x: T) => void = () => {} + private _reject: (err: any) => void = () => {} + + constructor () { + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve + this._reject = reject + }) + } + + get resolve (): typeof this._resolve { + return this._resolve + } + + get reject (): typeof this._reject { + return this._reject + } +} + +export function normalizeValue (value: string): string { + const trimmed = value.trim() + const numberRegex = /^[+\d()\s-]+$/ + if (numberRegex.test(trimmed)) { + const number = trimmed.replace(/[()\s-]+/g, '') + return number.startsWith('+') ? number : '+' + number + } else { + const domain = 't.me' + const tmeIndex = trimmed.indexOf(domain) + if (tmeIndex === -1) { + return trimmed.startsWith('@') ? trimmed : '@' + trimmed + } else { + return '@' + trimmed.slice(tmeIndex + domain.length + 1) + } + } +} + +export async function getFiles (msg: Api.Message): Promise { + if (msg.media == null) return [] + const props = getFileProps(msg.media) + if (props === undefined) return [] + const file = await msg.downloadMedia({}) + if (file == null) return [] + const obj: AttachedFile = { ...props, file: typeof file === 'string' ? Buffer.from(file) : file } + return [obj] +} + +export async function uploadFile ( + file: AttachedFile, + token: string, + opts?: { space: Ref, attachedTo: Ref } +): Promise { + const uploadUrl = config.UploadUrl + if (uploadUrl === undefined) { + throw Error('UploadURL is not defined') + } + + const data = new FormData() + data.append( + 'file', + Object.assign(file.file, { + lastModified: file.lastModified, + size: file.size, + name: file.name, + type: file.type + }) + ) + + const params = + opts !== undefined + ? [ + ['space', opts.space], + ['attachedTo', opts.attachedTo] + ] + .filter((x): x is [string, Ref] => x[1] !== undefined) + .map(([name, value]) => `${name}=${value}`) + .join('&') + : '' + const suffix = params === '' ? params : `?${params}` + + const url = `${uploadUrl}${suffix}` + const resp = await fetch(url, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + token + }, + body: data + }) + + if (resp.status !== 200) { + throw Error(`Failed to upload file: ${resp.statusText}`) + } + + return await resp.text() +} + +export async function createPlatformClient (token: string): Promise { + setMetadata(client.metadata.ClientSocketFactory, (url) => { + return new WebSocket(url, { + headers: { + 'User-Agent': config.ServiceID + } + }) as never as ClientSocket + }) + + const endpoint = await getTransactorEndpoint(token) + const connection = await createClient(token, endpoint) + + return connection +} + +export function getFileName (doc: Api.Document): string | undefined { + const fileNameAttr = doc.originalArgs.attributes.find((p) => p.className === 'DocumentAttributeFilename') + if (fileNameAttr !== undefined && fileNameAttr.className === 'DocumentAttributeFilename') { + return fileNameAttr.fileName + } + const titleAttr = doc.originalArgs.attributes.find((p) => p.className === 'DocumentAttributeAudio') + if (titleAttr !== undefined && titleAttr.className === 'DocumentAttributeAudio') { + return titleAttr.title + } +} + +export function getFileProps (media: Api.TypeMessageMedia): Omit | undefined { + if (media.className === 'MessageMediaDocument' && media.document?.className === 'Document') { + const lastModified = media.document.date * 1000 + const ext = mime.getExtension(media.document.mimeType) + const name = + ext !== null + ? `doc_${new Date(lastModified).toLocaleString()}.${ext}` + : `doc_${new Date(lastModified).toLocaleString()}` + return { + type: media.document.mimeType, + size: media.document.size as unknown as number, + lastModified, + name: getFileName(media.document) ?? name + } + } + if (media.className === 'MessageMediaPhoto' && media.photo?.className === 'Photo') { + const lastModified = media.photo.date * 1000 + const ext = mime.getExtension('image/jpeg') + const name = + ext !== null + ? `photo_${new Date(lastModified).toLocaleString()}.${ext}` + : `photo_${new Date(lastModified).toLocaleString()}` + return { + lastModified, + type: 'image/jpeg', + name + } + } +} diff --git a/services/telegram/pod-telegram/src/workspace.ts b/services/telegram/pod-telegram/src/workspace.ts new file mode 100644 index 0000000000..96de3776fd --- /dev/null +++ b/services/telegram/pod-telegram/src/workspace.ts @@ -0,0 +1,844 @@ +import attachment, { Attachment } from '@hcengineering/attachment' +import contact, { + Channel, + Employee, + Contact as PContact, + PersonAccount, + getFirstName, + getLastName +} from '@hcengineering/contact' +import core, { + Account, + AttachedDoc, + Blob, + Client, + Doc, + Hierarchy, + Ref, + Tx, + TxCollectionCUD, + TxCreateDoc, + TxFactory, + TxOperations, + TxProcessor, + TxRemoveDoc, + TxUpdateDoc +} from '@hcengineering/core' +import settingP from '@hcengineering/setting' +import telegramP, { NewTelegramMessage } from '@hcengineering/telegram' +import { encode } from 'jwt-simple' +import type { Collection } from 'mongodb' +import fetch from 'node-fetch' +import { Api } from 'telegram' +import config from './config' +import { platformToTelegram, telegramToPlatform } from './markup' +import { MsgQueue } from './queue' +import type { TelegramConnectionInterface } from './telegram' +import { telegram } from './telegram' +import { Event, LastMsgRecord, TelegramMessage, TgUser, UserRecord, WorkspaceChannel } from './types' +import { createPlatformClient, getFiles, normalizeValue, uploadFile } from './utils' + +export class WorkspaceWorker { + private readonly clients = new Map< + string, + { + conn: TelegramConnectionInterface + queue?: MsgQueue + } + >() + + private channels: Map = new Map() + private channelsById: Map, Channel> = new Map, Channel>() + private readonly hierarchy: Hierarchy + + private constructor ( + private readonly client: Client, + private readonly token: string, + private readonly workspace: string, + private readonly userStorage: Collection, + private readonly lastMsgStorage: Collection, + private readonly channelsStorage: Collection + ) { + // eslint-disable-next-line + this.client.notify = (...tx) => void this.txHandler(...tx) + this.hierarchy = this.client.getHierarchy() + } + + private async init (): Promise { + await this.initChannels() + const recs = await this.userStorage.find({ workspace: this.workspace }).toArray() + + await Promise.all( + recs.map(async (r) => { + await this.initClient(r) + }) + ) + + recs.forEach((rec) => { + void this.gatherMessages(rec) + void this.sendNewMsgs(rec) + }) + } + + private async initClient (rec: UserRecord): Promise { + const conn = await telegram.create(rec.phone, rec.token ?? '') + this.clients.set(rec.phone, { conn }) + } + + private async txHandler (...txes: Tx[]): Promise { + for (const tx of txes) { + switch (tx._class) { + case core.class.TxCreateDoc: { + await this.txCreateDoc(tx as TxCreateDoc) + return + } + case core.class.TxCollectionCUD: { + await this.txCollectionCUD(tx as TxCollectionCUD) + return + } + case core.class.TxUpdateDoc: { + await this.txUpdateDoc(tx as TxUpdateDoc) + return + } + case core.class.TxRemoveDoc: { + await this.txRemoveDoc(tx as TxRemoveDoc) + } + } + } + } + + private async txCreateDoc (tx: TxCreateDoc): Promise { + if (this.hierarchy.isDerived(tx.objectClass, contact.class.Channel)) { + await this.createChannelHandler(tx) + } + if (this.hierarchy.isDerived(tx.objectClass, telegramP.class.NewMessage)) { + const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc) + try { + await this.sendMsg(doc) + } catch (err: any) { + console.log(err) + } + } + } + + private async txCollectionCUD (tx: TxCollectionCUD): Promise { + await this.txHandler(tx.tx) + } + + private async txUpdateDoc (tx: TxUpdateDoc): Promise { + if (this.hierarchy.isDerived(tx.objectClass, contact.class.Channel)) { + await this.updateChannelHandler(tx) + } + if (this.hierarchy.isDerived(tx.objectClass, contact.mixin.Employee)) { + await this.handleEmployeeUpdate(tx) + } + } + + private async txRemoveDoc (tx: TxRemoveDoc): Promise { + if (this.hierarchy.isDerived(tx.objectClass, contact.class.Channel)) { + await this.removeChannelHandler(tx) + } + } + + async close (): Promise { + await Promise.all( + [...this.clients.values()].map(async (client) => { + await client.conn.close() + }) + ) + this.clients.clear() + } + + static async create ( + workspace: string, + userStorage: Collection, + lastMsgStorage: Collection, + channelsStorage: Collection + ): Promise { + const token = encode({ email: config.SystemEmail, workspace }, config.Secret) + const client = await createPlatformClient(token) + + const worker = new WorkspaceWorker(client, token, workspace, userStorage, lastMsgStorage, channelsStorage) + await worker.init() + return worker + } + + // #region Users + + async addUser ({ email, phone, conn }: TgUser): Promise { + const user = (await this.client.findAll(core.class.Account, { email }, { limit: 1 }))[0] + + if (user === undefined) { + throw Error(`Unable to find user by email: ${email}`) + } + + const token = conn.getToken() + + if (token === undefined) { + throw Error('Unable to get telegram token') + } + + const res = await this.userStorage.insertOne({ + userId: user._id, + email, + workspace: this.workspace, + phone, + token + }) + + const rec = await this.userStorage.findOne({ _id: res.insertedId }) + + if (rec === null) { + console.error( + `Something went wrong, failed to get inserted obj: ${ + this.userStorage.collectionName + }/${res.insertedId.toString()}` + ) + return + } + + this.clients.set(phone, { conn }) + + void this.gatherMessages(rec) + } + + async checkUsers (): Promise { + const employees = await this.client.findAll(contact.mixin.Employee, { active: false }) + const accounts = await this.client.findAll(contact.class.PersonAccount, { + person: { $in: employees.map((p) => p._id) } + }) + for (const acc of accounts) { + await this.deactivateUser(acc._id) + } + } + + private async deactivateUser (acc: Ref): Promise { + const res = await this.userStorage.findOne({ userId: acc, workspace: this.workspace }) + + if (res !== null) { + const client = this.clients.get(res.phone) + + if (client === undefined) { + return + } + + await client.conn.signOut() + await this.clearOutdatedConnection(res.phone) + } + } + + private filterUsers (users: Api.User[]): Api.User[] { + return users.filter((p) => this.getChannel(p) !== undefined) + } + + private async handleEmployeeUpdate (tx: TxUpdateDoc): Promise { + const ctx = tx as TxUpdateDoc + if (ctx.operations.active === false) { + const acc = await this.client.findOne(contact.class.PersonAccount, { person: ctx.objectId }) + if (acc !== undefined) { + await this.deactivateUser(acc._id) + } + } + } + + async removeUser ({ phone }: Pick): Promise { + const client = this.clients.get(phone) + + if (client === undefined) { + return + } + + await client.conn.signOut() + await this.clearOutdatedConnection(phone, true) + } + + private async clearOutdatedConnection (phone: string, signOut: boolean = false): Promise { + const client = this.clients.get(phone) + + if (client === undefined) { + return + } + + await client.conn.close() + this.clients.delete(phone) + const rec = await this.userStorage.findOne({ + workspace: this.workspace, + phone + }) + + if (rec === null) { + return + } + + await this.userStorage.deleteOne({ _id: rec._id }) + + const integration = await this.client.findOne(settingP.class.Integration, { + type: telegramP.integrationType.Telegram, + createdBy: rec.userId as Ref + }) + + if (integration === undefined) { + return + } + + const txOp = new TxOperations(this.client, core.account.System) + + if (signOut) { + console.log('Signout', this.workspace, phone) + await txOp.remove(integration) + } else { + console.log('Disable', this.workspace, phone) + await txOp.update(integration, { disabled: true }) + } + } + + // #endregion + + // #region Messages + + async sendMsg (msg: NewTelegramMessage): Promise { + const rec = await this.userStorage.findOne({ userId: msg.modifiedBy }) + if (rec === undefined || rec?.phone === undefined) return + const client = this.clients.get(rec.phone) + + if (client === undefined) { + throw Error('Failed to find connection') + } + + const channel = this.channelsById.get(msg.attachedTo as Ref) + if (channel === undefined) return + const target = normalizeValue(channel.value) + + const importRequired = await client.conn.isContactImportRequired(target) + + if (importRequired) { + const contact = await this.client.findOne(channel.attachedToClass, { + _id: channel.attachedTo as Ref + }) + if (contact === undefined) { + throw new Error("Couldn't find contact by id" + channel.attachedTo) + } + + await client.conn.addContact({ + lastName: getLastName(contact.name), + firstName: getFirstName(contact.name), + phone: target + }) + } + + const { message, entities } = platformToTelegram(msg.content) + const files = await this.getFiles(msg) + if (files.length < 2) { + const res = await client.conn.sendMsg(target, message, entities, files.shift()) + const user = await res.getChat() + + if (user?.className !== 'User') { + return + } + + client.queue?.add({ msg: res, user }) + } else { + for (let i = 0; i < files.length; i++) { + const file = files[i] + const res = + i === 0 + ? await client.conn.sendMsg(target, message, entities, file) + : await client.conn.sendMsg(target, '', [], file) + const user = await res.getChat() + + if (user?.className !== 'User') { + return + } + + client.queue?.add({ msg: res, user }) + } + } + + const factory = new TxFactory(msg.modifiedBy) + const tx = factory.createTxUpdateDoc(msg._class, msg.space, msg._id, { + status: 'sent' + }) + await this.client.tx(tx) + } + + private async getUserOlderMsgs (user: Api.User, record: UserRecord, conn: TelegramConnectionInterface): Promise { + const channels = this.getChannel(user) + if (channels === undefined) return + for (const channel of channels) { + await this.getUserOlderChannelMsgs(user, record, conn, channel) + } + } + + private async getLastMsg (user: Api.User, record: UserRecord, channelID: string): Promise { + try { + let res = await this.lastMsgStorage.findOne({ + workspace: this.workspace, + phone: record.phone, + channelID, + participantID: user.id.toString() + }) + + if (res === null) { + const lastMsgId = ( + await this.lastMsgStorage.insertOne({ + maxMsgId: 0, + minMsgId: 0, + participantID: user.id.toString(), + phone: record.phone, + channelID, + workspace: this.workspace + }) + ).insertedId + + res = await this.lastMsgStorage.findOne({ + _id: lastMsgId + }) + } + + return res + } catch (e) { + console.error(e) + return null + } + } + + async loadChannelUserMsgs ( + user: Api.User, + record: UserRecord, + conn: TelegramConnectionInterface, + channel: Channel + ): Promise { + const lastMsg = await this.getLastMsg(user, record, channel._id) + + if (lastMsg === null) { + console.error("Couldn't create last message record") + return 0 + } + try { + const msgs = conn.getMsgs(user, undefined, lastMsg.maxMsgId, 500) + for await (const msg of msgs) { + await this.savePlatformMessage(msg, user, record, channel, lastMsg) + } + } catch (e) { + console.error(e) + } + await this.lastMsgStorage.updateOne( + { + _id: lastMsg._id + }, + { + $set: { + minMsgId: lastMsg.minMsgId, + maxMsgId: lastMsg.maxMsgId + } + } + ) + return lastMsg.maxMsgId + } + + private async savePlatformMessage ( + msg: Api.Message, + user: Api.User, + record: UserRecord, + channel: Channel, + lastMsg: LastMsgRecord + ): Promise { + try { + const tx = this.makePlatformMsg({ msg, user }, record, channel) + await this.client.tx(tx) + void this.makePlatformAttachments({ msg, user }, record, tx) + lastMsg.maxMsgId = Math.max(lastMsg.maxMsgId, msg.id) + lastMsg.minMsgId = Math.min(lastMsg.minMsgId, msg.id) + if (lastMsg.minMsgId === 0) { + lastMsg.minMsgId = msg.id + } + } catch (e) { + console.error(e) + } + } + + async loadUserMsgs (user: Api.User, record: UserRecord, conn: TelegramConnectionInterface): Promise { + const channels = this.getChannel(user) + if (channels === undefined) return 0 + const ids: number[] = [] + for (const channel of channels) { + const id = await this.loadChannelUserMsgs(user, record, conn, channel) + ids.push(id) + } + let maxID = 0 + for (const id of ids) { + maxID = Math.max(id, maxID) + } + return maxID + } + + async getUserOlderChannelMsgs ( + user: Api.User, + record: UserRecord, + conn: TelegramConnectionInterface, + channel: Channel + ): Promise { + const lastMsg = await this.getLastMsg(user, record, channel._id) + + if (lastMsg === null) { + console.error("Couldn't create last message record") + return + } + + let userMinID: number = lastMsg.minMsgId + try { + const msgs = conn.getMsgs(user, lastMsg.minMsgId) + + for await (const msg of msgs) { + try { + const tx = this.makePlatformMsg({ msg, user }, record, channel) + await this.client.tx(tx) + await this.makePlatformAttachments({ msg, user }, record, tx) + if (msg.id < userMinID || userMinID === 0) { + userMinID = msg.id + await this.lastMsgStorage.updateOne( + { + _id: lastMsg._id + }, + { + $set: { minMsgId: userMinID } + } + ) + } + } catch (e) { + console.error(e) + continue + } + } + } catch (e) { + console.error(e) + } finally { + await this.lastMsgStorage.updateOne( + { + _id: lastMsg._id + }, + { + $set: { minMsgId: userMinID } + } + ) + } + } + + private async sendNewMsgs (record: UserRecord): Promise { + const newMessages = await this.client.findAll(telegramP.class.NewMessage, { + modifiedBy: record.userId as Ref, + status: 'new' + }) + for (const message of newMessages) { + try { + await this.sendMsg(message) + } catch (err: any) { + console.log('Error while sending new message', record.workspace, record.userId, JSON.stringify(err)) + } + } + } + + private makePlatformMsg ( + event: Event, + record: UserRecord, + channel: Channel + ): TxCollectionCUD { + const factory = new TxFactory(record.userId as Ref) + const tx = factory.createTxCollectionCUD( + channel._class, + channel._id, + channel.space, + 'items', + factory.createTxCreateDoc(telegramP.class.Message, core.space.Workspace, { + attachedTo: channel._id, + attachedToClass: channel._class, + collection: 'items', + sendOn: event.msg.date * 1000, + content: telegramToPlatform(event.msg), + incoming: event.msg.out !== true + }) + ) + tx.tx.modifiedOn = event.msg.date * 1000 + return tx + } + + private async gatherMessages (record: UserRecord): Promise { + const client = this.clients.get(record.phone) + + if (client === undefined) { + return + } + + const allUsers = await client.conn.getUsers().catch(() => []) + const users = this.filterUsers(allUsers) + + const queue = new MsgQueue(true, async (event: Event) => { + try { + const channels = this.getChannel(event.user) + if (channels === undefined) return + for (const channel of channels) { + const lastMsg = await this.getLastMsg(event.user, record, channel._id) + + if (lastMsg === null) { + console.error("Couldn't create last message record") + continue + } + await this.savePlatformMessage(event.msg, event.user, record, channel, lastMsg) + await this.lastMsgStorage.updateOne( + { + _id: lastMsg._id + }, + { + $set: { + minMsgId: lastMsg.minMsgId, + maxMsgId: lastMsg.maxMsgId + } + } + ) + } + } catch (e) { + console.error(e) + } + }) + client.queue = queue + + client.conn.sub((user, msg) => { + queue.add({ + user, + msg + }) + }) + const promises: Array> = [] + for (const user of users) { + promises.push(this.loadUserMsgs(user, record, client.conn)) + } + const ids = await Promise.all(promises) + let maxID = 0 + for (const id of ids) { + maxID = Math.max(id, maxID) + } + + queue.dropBefore(maxID) + queue.unpause() + for (const user of users) { + void this.getUserOlderMsgs(user, record, client.conn) + } + } + + // #region Attachments + + private async getFiles (msg: NewTelegramMessage): Promise { + const attachments = await this.client.findAll(attachment.class.Attachment, { attachedTo: msg._id }) + const res: Buffer[] = [] + for (const attachment of attachments) { + const buffer = await this.getAttachmentData(attachment.file) + if (buffer.length > 0) { + res.push( + Object.assign(buffer, { + name: attachment.name + }) + ) + } + } + return res + } + + private async makePlatformAttachments ( + event: Event, + record: UserRecord, + createTx: TxCollectionCUD + ): Promise { + const msg = TxProcessor.createDoc2Doc(createTx.tx as TxCreateDoc) + const factory = new TxFactory(record.userId as Ref) + const files = await getFiles(event.msg) + for (const file of files) { + try { + file.size = file.size ?? file.file.length + const uuid = await uploadFile(file, this.token, { space: msg.space, attachedTo: msg._id }) + const tx = factory.createTxCollectionCUD( + msg._class, + msg._id, + msg.space, + 'attachments', + factory.createTxCreateDoc(attachment.class.Attachment, msg.space, { + name: file.name, + file: uuid as Ref, + type: file.type, + size: file.size, + lastModified: file.lastModified, + collection: 'attachments', + attachedTo: msg._id, + attachedToClass: msg._class + }) + ) + tx.modifiedOn = event.msg.date * 1000 + tx.tx.modifiedOn = event.msg.date * 1000 + await this.client.tx(tx) + } catch (e) { + console.error(e) + continue + } + } + } + + private async getAttachmentData (fileId: string): Promise { + const uploadUrl = config.UploadUrl + if (uploadUrl === undefined) { + throw Error('UploadURL is not defined') + } + const url = `${uploadUrl}?file=${fileId}&token=${this.token}` + const res = await fetch(url) + const buffer = await res.buffer() + return buffer + } + + // #endregion + + // #endregion + + // #region Channels + + private async initChannels (): Promise { + const oldChannels = await this.channelsStorage + .find({ + workspace: this.workspace + }) + .toArray() + const oldChannelsSet = new Set(oldChannels.map((p) => p.value)) + const channels = await this.getChannels() + this.channels = new Map() + for (const channel of channels) { + this.setChannel(channel) + } + this.channelsById = new Map( + channels.map((p) => { + return [p._id, p] + }) + ) + for (const channel of channels) { + if (!oldChannelsSet.has(channel.value)) { + void this.addChannel(channel) + } + } + for (const oldChannel of Array.from(oldChannelsSet)) { + const value = normalizeValue(oldChannel) + if (!this.channels.has(value)) { + await this.channelsStorage.deleteOne({ + workspace: this.workspace, + value: oldChannel + }) + } + } + } + + private getChannel (user: Api.User): Channel[] | undefined { + let result: Channel[] | undefined + if (user.username != null) { + result = this.channels.get(`@${user.username}`) + } + if (result === undefined && user.phone != null) { + result = this.channels.get(`+${user.phone}`) + } + return result + } + + private async getChannels (): Promise { + return await this.client.findAll(contact.class.Channel, { + provider: contact.channelProvider.Telegram + }) + } + + private async addChannel (channel: Channel): Promise { + this.setChannel(channel) + this.channelsById.set(channel._id, channel) + await this.newChannel(channel) + await this.channelsStorage.insertOne({ + workspace: this.workspace, + value: channel.value + }) + } + + private async removeChannel (channel: Channel): Promise { + const value = normalizeValue(channel.value) + const currentChannels = this.channels.get(value) + if (currentChannels !== undefined) { + const index = currentChannels.findIndex((p) => p._id === channel._id) + if (index > -1) { + currentChannels.splice(index, 1) + } + if (currentChannels.length === 0) { + this.channels.delete(value) + } + } + this.channelsById.delete(channel._id) + await this.lastMsgStorage.deleteMany({ + channelID: channel._id + }) + await this.channelsStorage.deleteOne({ + workspace: this.workspace, + value: channel.value + }) + } + + private setChannel (channel: Channel): void { + const value = normalizeValue(channel.value) + const current = this.channels.get(value) + if (current === undefined) { + this.channels.set(value, [channel]) + } else { + const index = current.findIndex((p) => p._id === channel._id) + if (index === -1) { + current.push(channel) + } + } + } + + private async newChannel (channel: Channel): Promise { + const recs = await this.userStorage.find({ workspace: this.workspace }).toArray() + for (const record of recs) { + const client = this.clients.get(record.phone) + + if (client === undefined) { + return + } + + const user = await client.conn.getUser(channel.value) + if (user === undefined) continue + + await this.loadChannelUserMsgs(user, record, client.conn, channel) + + await this.getUserOlderChannelMsgs(user, record, client.conn, channel) + } + } + + // #region TxHandlers + + private async createChannelHandler (tx: TxCreateDoc): Promise { + const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc) + if (doc.provider === contact.channelProvider.Telegram) { + await this.addChannel(doc) + } + } + + private async updateChannelHandler (tx: TxUpdateDoc): Promise { + const ctx = tx as TxUpdateDoc + const channel = this.channelsById.get(ctx.objectId) + if (channel !== undefined && ctx.operations.value !== undefined) { + await this.removeChannel(channel) + channel.value = ctx.operations.value + await this.addChannel(channel) + } + } + + private async removeChannelHandler (tx: TxRemoveDoc): Promise { + const ctx = tx as TxRemoveDoc + const channel = this.channelsById.get(ctx.objectId) + if (channel !== undefined) { + await this.removeChannel(channel) + } + } + + // #endregion + + // #endregion +} diff --git a/services/telegram/pod-telegram/tsconfig.json b/services/telegram/pod-telegram/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/services/telegram/pod-telegram/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/tests/.env b/tests/.env index 90fe452486..c845845dfc 100644 --- a/tests/.env +++ b/tests/.env @@ -1 +1,2 @@ -STORAGE_CONFIG="minio|minio?accessKey=minioadmin&secretKey=minioadmin" \ No newline at end of file +STORAGE_CONFIG="minio|minio?accessKey=minioadmin&secretKey=minioadmin" +SERVER_PROVIDER="ws" \ No newline at end of file diff --git a/tests/docker-compose.yaml b/tests/docker-compose.yaml index 538a70bf61..2cb6b68034 100644 --- a/tests/docker-compose.yaml +++ b/tests/docker-compose.yaml @@ -1,4 +1,3 @@ -version: "3.8" services: mongodb: image: 'mongo:7-jammy' @@ -72,7 +71,7 @@ services: - ELASTIC_URL=http://elastic:9200 - GMAIL_URL=http://localhost:8088 - CALENDAR_URL=http://localhost:8095 - - REKONI_URL=http://rekoni:4005 + - REKONI_URL=http://rekoni:4007 - TELEGRAM_URL=http://localhost:8086 - COLLABORATOR_URL=ws://localhost:3079 - COLLABORATOR_API_URL=http://localhost:3079 @@ -100,7 +99,7 @@ services: - METRICS_CONSOLE=false - METRICS_FILE=metrics.txt - STORAGE_CONFIG=${STORAGE_CONFIG} - - REKONI_URL=http://rekoni:4005 + - REKONI_URL=http://rekoni:7 - FRONT_URL=http://localhost:8083 - UPLOAD_URL=http://localhost:8083/files - ACCOUNTS_URL=http://account:3003 @@ -127,7 +126,7 @@ services: image: hardcoreeng/rekoni-service restart: on-failure ports: - - 4005:4004 + - 4007:4004 deploy: resources: limits: