diff --git a/.circleci/build.sh b/.circleci/build.sh index 517318343..18ea3ba6e 100755 --- a/.circleci/build.sh +++ b/.circleci/build.sh @@ -10,27 +10,16 @@ fi # enables building the test-deployment container with the same script # defaults to packages for minimal intervention in the ci config FOLDER="${FOLDER:-packages}" -SHOULD_PUBLISH="${SHOULD_PUBLISH:-false}" -DOCKER_IMAGE_TAG="speckle/speckle-${SPECKLE_SERVER_PACKAGE}" +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +# shellcheck disable=SC1090,SC1091 +source "${SCRIPT_DIR}/common.sh" -# IMAGE_VERSION_TAG=$(./.circleci/get_version.sh) -# if there is not image version tag, uses the SHA1 of the last git commit of the branch that triggered this build -IMAGE_VERSION_TAG="${IMAGE_VERSION_TAG:-${CIRCLE_SHA1}}" -echo "${IMAGE_VERSION_TAG}" +echo "Building image: ${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" export DOCKER_BUILDKIT=1 -docker build --build-arg SPECKLE_SERVER_VERSION="${IMAGE_VERSION_TAG}" -t "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" . --file "${FOLDER}/${SPECKLE_SERVER_PACKAGE}/Dockerfile" +docker build --build-arg SPECKLE_SERVER_VERSION="${IMAGE_VERSION_TAG}" --tag "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" --file "${FOLDER}/${SPECKLE_SERVER_PACKAGE}/Dockerfile" . -if [[ "${SHOULD_PUBLISH}" == "true" ]]; then - echo "publishing images" - docker tag "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" "${DOCKER_IMAGE_TAG}:latest" - - if [[ "${IMAGE_VERSION_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - docker tag "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" "${DOCKER_IMAGE_TAG}:2" - fi - - echo "${DOCKER_REG_PASS}" | docker login -u "${DOCKER_REG_USER}" --password-stdin "${DOCKER_REG_URL}" - docker push --all-tags "${DOCKER_IMAGE_TAG}" -fi +echo " Saving image: ${DOCKER_FILE_NAME}" +docker save --output "/tmp/ci/workspace/${DOCKER_FILE_NAME}" "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" diff --git a/.circleci/common.sh b/.circleci/common.sh new file mode 100755 index 000000000..4abeb82fc --- /dev/null +++ b/.circleci/common.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -eo pipefail + +DOCKER_IMAGE_TAG="speckle/speckle-${SPECKLE_SERVER_PACKAGE}" +IMAGE_VERSION_TAG="${IMAGE_VERSION_TAG:-${CIRCLE_SHA1}}" +# shellcheck disable=SC2034,SC2086 +DOCKER_FILE_NAME="$(echo ${DOCKER_IMAGE_TAG}_${IMAGE_VERSION_TAG} | sed -e 's/[^A-Za-z0-9._-]/_/g')" diff --git a/.circleci/config.yml b/.circleci/config.yml index 28c6e8c90..0cd52fe36 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,9 +23,7 @@ workflows: - pre-commit: filters: *filters-everything - - docker-build-and-publish-server: - context: &docker-hub-context - - docker-hub + - docker-build-server: filters: &filters-build tags: only: /.*/ @@ -33,59 +31,114 @@ workflows: - test-server - get-version - should-build - - should-publish - - docker-build-and-publish-frontend: - context: *docker-hub-context + - docker-build-frontend: filters: *filters-build requires: - get-version - should-build - - should-publish - - docker-build-and-publish-webhooks: - context: *docker-hub-context + - docker-build-webhooks: filters: *filters-build requires: - get-version - test-server - should-build - - should-publish - - docker-build-and-publish-file-imports: - context: *docker-hub-context + - docker-build-file-imports: filters: *filters-build requires: - get-version - test-server - should-build - - should-publish - - docker-build-and-publish-previews: - context: *docker-hub-context + - docker-build-previews: filters: *filters-build requires: - get-version - test-server - should-build - - should-publish - - docker-build-and-publish-test-container: - context: *docker-hub-context + - docker-build-test-container: filters: *filters-build requires: - get-version - test-server - should-build - - should-publish - - docker-build-and-publish-monitor-container: - context: *docker-hub-context + - docker-build-monitor-container: filters: *filters-build requires: - get-version - should-build + + - docker-publish-server: + context: &docker-hub-context + - docker-hub + filters: &filters-publish + branches: + ignore: /pull\/[0-9]+/ + tags: + only: /.*/ + requires: + - get-version - should-publish + - docker-build-server + - pre-commit + + - docker-publish-frontend: + context: *docker-hub-context + filters: *filters-publish + requires: + - get-version + - should-publish + - docker-build-frontend + - pre-commit + + - docker-publish-webhooks: + context: *docker-hub-context + filters: *filters-publish + requires: + - get-version + - should-publish + - docker-build-webhooks + - pre-commit + + - docker-publish-file-imports: + context: *docker-hub-context + filters: *filters-publish + requires: + - get-version + - should-publish + - docker-build-file-imports + - pre-commit + + - docker-publish-previews: + context: *docker-hub-context + filters: *filters-publish + requires: + - get-version + - should-publish + - docker-build-previews + - pre-commit + + - docker-publish-test-container: + context: *docker-hub-context + filters: *filters-publish + requires: + - get-version + - should-publish + - docker-build-test-container + - pre-commit + + - docker-publish-monitor-container: + context: *docker-hub-context + filters: *filters-publish + requires: + - get-version + - should-publish + - docker-build-monitor-container + - pre-commit - publish-helm-chart: filters: &filters-publish @@ -96,16 +149,15 @@ workflows: tags: only: &filters-tag /^[0-9]+\.[0-9]+\.[0-9]+$/ requires: - - test-server - get-version - should-publish - - docker-build-and-publish-server - - docker-build-and-publish-frontend - - docker-build-and-publish-webhooks - - docker-build-and-publish-file-imports - - docker-build-and-publish-previews - - docker-build-and-publish-monitor-container - - docker-build-and-publish-test-container + - docker-publish-server + - docker-publish-frontend + - docker-publish-webhooks + - docker-publish-file-imports + - docker-publish-previews + - docker-publish-monitor-container + - docker-publish-test-container - publish-npm: filters: @@ -125,7 +177,6 @@ jobs: working_directory: &work-dir /tmp/ci steps: - checkout - - run: pwd - run: mkdir -p workspace - run: name: set version @@ -142,13 +193,12 @@ jobs: docker: - image: cimg/base:2022.08 working_directory: *work-dir - environment: - # £ delimited strings of regex for matches which should be published + environment: &publishable-tags-branches PUBLISHABLE_TAGS: '^[0-9]+\.[0-9]+\.[0-9]+$' + # £ delimited strings of regex for matches which should be published PUBLISHABLE_BRANCHES: '^main$£^hotfix.*£^alpha.*' steps: - checkout - - run: pwd - run: mkdir -p workspace - run: name: determine whether to publish @@ -165,9 +215,9 @@ jobs: docker: - image: cimg/base:2022.08 working_directory: *work-dir + environment: *publishable-tags-branches steps: - checkout - - run: pwd - run: mkdir -p workspace - run: name: determine whether to build @@ -197,7 +247,7 @@ jobs: type: string docker: - image: speckle/pre-commit-runner:latest - resource_class: large + resource_class: medium working_directory: *work-dir steps: - checkout @@ -259,6 +309,7 @@ jobs: S3_SECRET_KEY: 'minioadmin' S3_BUCKET: 'speckle-server' S3_CREATE_BUCKET: 'true' + REDIS_URL: 'redis://localhost:6379' S3_REGION: '' # optional, defaults to 'us-east-1' steps: - checkout @@ -304,10 +355,10 @@ jobs: path: packages/server/coverage/lcov-report destination: package/server/coverage - docker-build-and-publish: &docker-job + docker-build: &build-job docker: &docker-image - image: cimg/node:16.15 - resource_class: xlarge + resource_class: medium working_directory: *work-dir steps: - checkout @@ -315,7 +366,11 @@ jobs: at: /tmp/ci/workspace - run: cat workspace/env-vars >> $BASH_ENV - run: cat workspace/should-build >> $BASH_ENV - - run: cat workspace/should-publish >> $BASH_ENV + - run: + name: 'Check if should proceed' + command: | + [[ "${SHOULD_BUILD}" != true ]] && echo "Should not build, stopping" && circleci-agent step halt + echo 'Proceeding with build' - setup_remote_docker: # a weird issue with yarn installing packages throwing EPERM errors # this fixes it @@ -324,40 +379,106 @@ jobs: - run: name: Build and Publish command: ./.circleci/build.sh + - persist_to_workspace: + root: workspace + paths: + - speckle* - docker-build-and-publish-server: - <<: *docker-job + docker-build-server: + <<: *build-job environment: SPECKLE_SERVER_PACKAGE: server - docker-build-and-publish-frontend: - <<: *docker-job + docker-build-frontend: + <<: *build-job environment: SPECKLE_SERVER_PACKAGE: frontend - docker-build-and-publish-previews: - <<: *docker-job + docker-build-previews: + <<: *build-job environment: SPECKLE_SERVER_PACKAGE: preview-service - docker-build-and-publish-webhooks: - <<: *docker-job + docker-build-webhooks: + <<: *build-job environment: SPECKLE_SERVER_PACKAGE: webhook-service - docker-build-and-publish-file-imports: - <<: *docker-job + docker-build-file-imports: + <<: *build-job environment: SPECKLE_SERVER_PACKAGE: fileimport-service - docker-build-and-publish-test-container: - <<: *docker-job + docker-build-test-container: + <<: *build-job environment: FOLDER: utils SPECKLE_SERVER_PACKAGE: test-deployment - docker-build-and-publish-monitor-container: - <<: *docker-job + docker-build-monitor-container: + <<: *build-job + environment: + FOLDER: utils + SPECKLE_SERVER_PACKAGE: monitor-deployment + + docker-publish: &publish-job + docker: &base-image + - image: cimg/base:2022.08 + resource_class: medium + working_directory: *work-dir + steps: + - checkout + - attach_workspace: + at: /tmp/ci/workspace + - run: cat workspace/env-vars >> $BASH_ENV + - run: cat workspace/should-publish >> $BASH_ENV + - run: + name: 'Check if should proceed' + command: | + [[ "${SHOULD_PUBLISH}" != true ]] && echo "Should not publish, stopping" && circleci-agent step halt + echo 'Proceeding with publish' + - setup_remote_docker: + # a weird issue with yarn installing packages throwing EPERM errors + # this fixes it + version: 20.10.12 + docker_layer_caching: true + - run: + name: Publish + command: ./.circleci/publish.sh + + docker-publish-server: + <<: *publish-job + environment: + SPECKLE_SERVER_PACKAGE: server + + docker-publish-frontend: + <<: *publish-job + environment: + SPECKLE_SERVER_PACKAGE: frontend + + docker-publish-previews: + <<: *publish-job + environment: + SPECKLE_SERVER_PACKAGE: preview-service + + docker-publish-webhooks: + <<: *publish-job + environment: + SPECKLE_SERVER_PACKAGE: webhook-service + + docker-publish-file-imports: + <<: *publish-job + environment: + SPECKLE_SERVER_PACKAGE: fileimport-service + + docker-publish-test-container: + <<: *publish-job + environment: + FOLDER: utils + SPECKLE_SERVER_PACKAGE: test-deployment + + docker-publish-monitor-container: + <<: *publish-job environment: FOLDER: utils SPECKLE_SERVER_PACKAGE: monitor-deployment @@ -389,7 +510,7 @@ jobs: - run: name: auth to npm as Speckle command: | - echo "npmRegistryServer: https://registry.npmjs.org/" >> .yarnrc.yml + echo "npmRegistryServer: https://registry.npmjs.org/" >> .yarnrc.yml echo "npmAuthToken: ${NPM_TOKEN}" >> .yarnrc.yml - run: name: try login to npm @@ -407,14 +528,6 @@ jobs: name: publish to npm command: 'yarn workspaces foreach -pv --no-private npm publish --access public' - # - run: - # name: commit changes - # command: | - # yarn prettier:fix - # git add . - # git commit -m '[ci skip] bump version to $IMAGE_VERSION_TAG' - # git push - publish-helm-chart: docker: *docker-image working_directory: *work-dir diff --git a/.circleci/publish.sh b/.circleci/publish.sh new file mode 100755 index 000000000..b7a915db8 --- /dev/null +++ b/.circleci/publish.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -eo pipefail + +SHOULD_PUBLISH="${SHOULD_PUBLISH:-false}" + +if [[ "${SHOULD_PUBLISH}" != "true" ]]; then + echo "Not publishing as the SHOULD_PUBLISH environment variable is not 'true'." + exit 0 +fi + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +# shellcheck disable=SC1090,SC1091 +source "${SCRIPT_DIR}/common.sh" + +echo "Publishing: ${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" + +echo "💾 Loading image" +docker load --input "/tmp/ci/workspace/${DOCKER_FILE_NAME}" + +echo "🐳 Publishing image" +docker tag "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" "${DOCKER_IMAGE_TAG}:latest" + +if [[ "${IMAGE_VERSION_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + docker tag "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" "${DOCKER_IMAGE_TAG}:2" +fi + +echo "${DOCKER_REG_PASS}" | docker login -u "${DOCKER_REG_USER}" --password-stdin "${DOCKER_REG_URL}" +docker push --all-tags "${DOCKER_IMAGE_TAG}" diff --git a/.circleci/should_build.sh b/.circleci/should_build.sh index 9fcf29a06..22d81c8fd 100755 --- a/.circleci/should_build.sh +++ b/.circleci/should_build.sh @@ -1,6 +1,12 @@ #!/bin/bash set -eo pipefail +IFS='£' read -r -a PUB_TAGS <<< "${PUBLISHABLE_TAGS}" +# shellcheck disable=SC2068 +for item in ${PUB_TAGS[@]}; do + [[ "${CIRCLE_TAG}" =~ ${item} ]] && echo "true" && exit 0 +done + # it's on the main branch [[ "${CIRCLE_BRANCH}" == "main" ]] && echo "true" && exit 0 diff --git a/.gitguardian.yml b/.gitguardian.yml index 662fb6565..fc3f2171e 100644 --- a/.gitguardian.yml +++ b/.gitguardian.yml @@ -1,3 +1,3 @@ matches-ignore: - name: MIXPANEL_TOKEN - match: acd87c5a50b56df91a795e999812a3a4 + - name: MIXPANEL_TOKEN + match: acd87c5a50b56df91a795e999812a3a4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9e7e912a6..1b743d9c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,5 +16,10 @@ repos: hooks: - id: helmlint + - repo: https://github.com/syntaqx/git-hooks + rev: 'v0.0.17' + hooks: + - id: circleci-config-validate + ci: autoupdate_schedule: quarterly diff --git a/package.json b/package.json index 179923c85..5567eba87 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dev": "yarn workspaces foreach -piv -j unlimited run dev", "dev:no-server": "yarn workspaces foreach --exclude @speckle/server -piv -j unlimited run dev", "dev:minimal": "yarn workspaces foreach -piv -j unlimited --include '{@speckle/server,@speckle/frontend}' run dev", + "gqlgen": "yarn workspaces foreach -piv -j unlimited --include '{@speckle/server,@speckle/frontend}' run gqlgen", "dev:server": "yarn workspace @speckle/server dev", "dev:frontend": "yarn workspace @speckle/frontend dev", "prepare": "husky install", @@ -40,7 +41,7 @@ "resolutions": { "tslib": "^2.3.1", "core-js": "3.22.4", - "vue-cli-plugin-apollo/graphql": "^15", + "graphql": "^15", "typescript": "^4.5.4", "vue-loader": "^15.10.0" }, diff --git a/packages/frontend/.eslintrc.js b/packages/frontend/.eslintrc.js index 2a67cf571..a55211aa6 100644 --- a/packages/frontend/.eslintrc.js +++ b/packages/frontend/.eslintrc.js @@ -26,7 +26,8 @@ const config = { extends: ['plugin:vue/recommended', '@vue/eslint-config-typescript', 'prettier'], rules: { 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': ['error'] + '@typescript-eslint/no-unused-vars': ['error'], + 'vue/component-name-in-template-casing': ['warn', 'kebab-case'] } }, { diff --git a/packages/frontend/codegen.yml b/packages/frontend/codegen.yml index 9fc3aa0ca..b9e456eb1 100644 --- a/packages/frontend/codegen.yml +++ b/packages/frontend/codegen.yml @@ -15,3 +15,5 @@ generates: config: scalars: JSONObject: Record + DateTime: string + dedupeFragments: true diff --git a/packages/frontend/src/config/apolloConfig.ts b/packages/frontend/src/config/apolloConfig.ts index 5bb5282db..bb8dc7365 100644 --- a/packages/frontend/src/config/apolloConfig.ts +++ b/packages/frontend/src/config/apolloConfig.ts @@ -110,6 +110,9 @@ function createCache(): InMemoryCache { }, pendingCollaborators: { merge: incomingOverwritesExistingMergeFunction + }, + pendingAccessRequests: { + merge: incomingOverwritesExistingMergeFunction } } }, diff --git a/packages/frontend/src/graphql/accessRequests.ts b/packages/frontend/src/graphql/accessRequests.ts new file mode 100644 index 000000000..c8e40d40f --- /dev/null +++ b/packages/frontend/src/graphql/accessRequests.ts @@ -0,0 +1,32 @@ +import { basicStreamAccessRequestFieldsFragment } from '@/graphql/fragments/accessRequests' +import { gql } from '@apollo/client/core' + +export const getStreamAccessRequestQuery = gql` + query GetStreamAccessRequest($streamId: String!) { + streamAccessRequest(streamId: $streamId) { + ...BasicStreamAccessRequestFields + } + } + + ${basicStreamAccessRequestFieldsFragment} +` + +export const createStreamAccessRequestMutation = gql` + mutation CreateStreamAccessRequest($streamId: String!) { + streamAccessRequestCreate(streamId: $streamId) { + ...BasicStreamAccessRequestFields + } + } + + ${basicStreamAccessRequestFieldsFragment} +` + +export const useStreamAccessRequestMutation = gql` + mutation UseStreamAccessRequest( + $requestId: String! + $accept: Boolean! + $role: StreamRole = STREAM_CONTRIBUTOR + ) { + streamAccessRequestUse(requestId: $requestId, accept: $accept, role: $role) + } +` diff --git a/packages/frontend/src/graphql/fragments/accessRequests.ts b/packages/frontend/src/graphql/fragments/accessRequests.ts new file mode 100644 index 000000000..4cf58aa73 --- /dev/null +++ b/packages/frontend/src/graphql/fragments/accessRequests.ts @@ -0,0 +1,22 @@ +import { limitedUserFieldsFragment } from '@/graphql/fragments/user' +import { gql } from '@apollo/client/core' + +export const basicStreamAccessRequestFieldsFragment = gql` + fragment BasicStreamAccessRequestFields on StreamAccessRequest { + id + streamId + createdAt + } +` + +export const fullStreamAccessRequestFieldsFragment = gql` + fragment FullStreamAccessRequestFields on StreamAccessRequest { + ...BasicStreamAccessRequestFields + requester { + ...LimitedUserFields + } + } + + ${limitedUserFieldsFragment} + ${basicStreamAccessRequestFieldsFragment} +` diff --git a/packages/frontend/src/graphql/fragments/streams.ts b/packages/frontend/src/graphql/fragments/streams.ts new file mode 100644 index 000000000..713ba5bb6 --- /dev/null +++ b/packages/frontend/src/graphql/fragments/streams.ts @@ -0,0 +1,12 @@ +import { fullStreamAccessRequestFieldsFragment } from '@/graphql/fragments/accessRequests' +import { gql } from '@apollo/client/core' + +export const streamPendingAccessRequestsFragment = gql` + fragment StreamPendingAccessRequests on Stream { + pendingAccessRequests { + ...FullStreamAccessRequestFields + } + } + + ${fullStreamAccessRequestFieldsFragment} +` diff --git a/packages/frontend/src/graphql/generated/graphql.ts b/packages/frontend/src/graphql/generated/graphql.ts index 8bc6dfe55..0e0bb1cdb 100644 --- a/packages/frontend/src/graphql/generated/graphql.ts +++ b/packages/frontend/src/graphql/generated/graphql.ts @@ -15,7 +15,7 @@ export type Scalars = { /** The `BigInt` scalar type represents non-fractional signed whole numeric values. */ BigInt: any; /** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ - DateTime: any; + DateTime: string; EmailAddress: any; /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ JSONObject: Record; @@ -393,6 +393,16 @@ export type CommitUpdateInput = { streamId: Scalars['String']; }; +export enum DiscoverableStreamsSortType { + CreatedDate = 'CREATED_DATE', + FavoritesCount = 'FAVORITES_COUNT' +} + +export type DiscoverableStreamsSortingInput = { + direction: SortDirection; + type: DiscoverableStreamsSortType; +}; + export type FileUpload = { __typename?: 'FileUpload'; branchName?: Maybe; @@ -467,10 +477,16 @@ export type Mutation = { /** Re-send a pending invite */ inviteResend: Scalars['Boolean']; objectCreate: Array>; + /** (Re-)send the account verification e-mail */ + requestVerification: Scalars['Boolean']; serverInfoUpdate?: Maybe; serverInviteBatchCreate: Scalars['Boolean']; /** Invite a new user to the speckle server and return the invite ID */ serverInviteCreate: Scalars['Boolean']; + /** Request access to a specific stream */ + streamAccessRequestCreate: StreamAccessRequest; + /** Accept or decline a stream access request. Must be a stream owner to invoke this. */ + streamAccessRequestUse: Scalars['Boolean']; /** Creates a new stream. */ streamCreate?: Maybe; /** Deletes an existing stream. */ @@ -639,6 +655,18 @@ export type MutationServerInviteCreateArgs = { }; +export type MutationStreamAccessRequestCreateArgs = { + streamId: Scalars['String']; +}; + + +export type MutationStreamAccessRequestUseArgs = { + accept: Scalars['Boolean']; + requestId: Scalars['String']; + role?: StreamRole; +}; + + export type MutationStreamCreateArgs = { stream: StreamCreateInput; }; @@ -797,6 +825,27 @@ export type ObjectCreateInput = { streamId: Scalars['String']; }; +export type PasswordStrengthCheckFeedback = { + __typename?: 'PasswordStrengthCheckFeedback'; + suggestions: Array; + warning?: Maybe; +}; + +export type PasswordStrengthCheckResults = { + __typename?: 'PasswordStrengthCheckResults'; + /** Verbal feedback to help choose better passwords. set when score <= 2. */ + feedback: PasswordStrengthCheckFeedback; + /** + * Integer from 0-4 (useful for implementing a strength bar): + * 0 too guessable: risky password. (guesses < 10^3) + * 1 very guessable: protection from throttled online attacks. (guesses < 10^6) + * 2 somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8) + * 3 safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10) + * 4 very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10) + */ + score: Scalars['Int']; +}; + export type PendingStreamCollaborator = { __typename?: 'PendingStreamCollaborator'; id: Scalars['String']; @@ -817,6 +866,7 @@ export type Query = { __typename?: 'Query'; /** Stare into the void. */ _?: Maybe; + /** All the streams of the server. Available to admins only. */ adminStreams?: Maybe; /** * Get all (or search for specific) users, registered or invited, from the server in a paginated view. @@ -836,6 +886,8 @@ export type Query = { comments?: Maybe; /** Commit/Object viewer state (local-only) */ commitObjectViewerState: CommitObjectViewerState; + /** All of the discoverable streams of the server */ + discoverableStreams?: Maybe; serverInfo: ServerInfo; serverStats: ServerStats; /** @@ -843,6 +895,8 @@ export type Query = { * to see it. */ stream?: Maybe; + /** Get authed user's stream access request */ + streamAccessRequest?: Maybe; /** * Look for an invitation to a stream, for the current user (authed or not). If token * isn't specified, the server will look for any valid invite. @@ -852,12 +906,10 @@ export type Query = { streamInvites: Array; /** All the streams of the current user, pass in the `query` parameter to search by name, description or ID. */ streams?: Maybe; - /** - * Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header). - * If ID is provided, admin access is required - */ + /** Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header). */ user?: Maybe; - userPwdStrength?: Maybe; + /** Validate password strength */ + userPwdStrength: PasswordStrengthCheckResults; /** * Search for users and return limited metadata about them, if you have the server:user role. * The query looks for matches in name & email @@ -902,11 +954,23 @@ export type QueryCommentsArgs = { }; +export type QueryDiscoverableStreamsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; + sort?: InputMaybe; +}; + + export type QueryStreamArgs = { id: Scalars['String']; }; +export type QueryStreamAccessRequestArgs = { + streamId: Scalars['String']; +}; + + export type QueryStreamInviteArgs = { streamId: Scalars['String']; token?: InputMaybe; @@ -1084,6 +1148,11 @@ export type SmartTextEditorValue = { version: Scalars['String']; }; +export enum SortDirection { + Asc = 'ASC', + Desc = 'DESC' +} + export type Stream = { __typename?: 'Stream'; /** All the recent activity on this stream in chronological order */ @@ -1119,9 +1188,17 @@ export type Stream = { /** Returns a list of all the file uploads for this stream. */ fileUploads?: Maybe>>; id: Scalars['String']; + /** + * Whether the stream (if public) can be found on public stream exploration pages + * and searches + */ + isDiscoverable: Scalars['Boolean']; + /** Whether the stream can be viewed by non-contributors */ isPublic: Scalars['Boolean']; name: Scalars['String']; object?: Maybe; + /** Pending stream access requests */ + pendingAccessRequests?: Maybe>; /** Collaborators who have been invited, but not yet accepted. */ pendingCollaborators?: Maybe>; /** Your role for this stream. `null` if request is not authenticated, or the stream is not explicitly shared with you. */ @@ -1189,6 +1266,18 @@ export type StreamWebhooksArgs = { id?: InputMaybe; }; +/** Created when a user requests to become a contributor on a stream */ +export type StreamAccessRequest = { + __typename?: 'StreamAccessRequest'; + createdAt: Scalars['DateTime']; + id: Scalars['ID']; + requester: LimitedUser; + requesterId: Scalars['String']; + /** Can only be selected if authed user has proper access */ + stream: Stream; + streamId: Scalars['String']; +}; + export type StreamCollaborator = { __typename?: 'StreamCollaborator'; avatar?: Maybe; @@ -1207,6 +1296,12 @@ export type StreamCollection = { export type StreamCreateInput = { description?: InputMaybe; + /** + * Whether the stream (if public) can be found on public stream exploration pages + * and searches + */ + isDiscoverable?: InputMaybe; + /** Whether the stream can be viewed by non-contributors */ isPublic?: InputMaybe; name?: InputMaybe; /** Optionally specify user IDs of users that you want to invite to be contributors to this stream */ @@ -1237,6 +1332,12 @@ export type StreamUpdateInput = { allowPublicComments?: InputMaybe; description?: InputMaybe; id: Scalars['String']; + /** + * Whether the stream (if public) can be found on public stream exploration pages + * and searches + */ + isDiscoverable?: InputMaybe; + /** Whether the stream can be viewed by non-contributors */ isPublic?: InputMaybe; name?: InputMaybe; }; @@ -1378,13 +1479,14 @@ export type User = { email?: Maybe; /** All the streams that a user has favorited */ favoriteStreams?: Maybe; + /** Whether the user has a pending/active email verification token */ + hasPendingVerification?: Maybe; id: Scalars['String']; name?: Maybe; profiles?: Maybe; role?: Maybe; /** All the streams that a user has access to. */ streams?: Maybe; - suuid?: Maybe; timeline?: Maybe; /** Total amount of favorites attached to streams owned by the user */ totalOwnedStreamsFavorites: Scalars['Int']; @@ -1531,6 +1633,29 @@ export type WebhookUpdateInput = { url?: InputMaybe; }; +export type GetStreamAccessRequestQueryVariables = Exact<{ + streamId: Scalars['String']; +}>; + + +export type GetStreamAccessRequestQuery = { __typename?: 'Query', streamAccessRequest?: { __typename?: 'StreamAccessRequest', id: string, streamId: string, createdAt: string } | null }; + +export type CreateStreamAccessRequestMutationVariables = Exact<{ + streamId: Scalars['String']; +}>; + + +export type CreateStreamAccessRequestMutation = { __typename?: 'Mutation', streamAccessRequestCreate: { __typename?: 'StreamAccessRequest', id: string, streamId: string, createdAt: string } }; + +export type UseStreamAccessRequestMutationVariables = Exact<{ + requestId: Scalars['String']; + accept: Scalars['Boolean']; + role?: InputMaybe; +}>; + + +export type UseStreamAccessRequestMutation = { __typename?: 'Mutation', streamAccessRequestUse: boolean }; + export type StreamWithBranchQueryVariables = Exact<{ streamId: Scalars['String']; branchName: Scalars['String']; @@ -1538,7 +1663,7 @@ export type StreamWithBranchQueryVariables = Exact<{ }>; -export type StreamWithBranchQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, branch?: { __typename?: 'Branch', id: string, name: string, description?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Commit', id: string, authorName?: string | null, authorId?: string | null, authorAvatar?: string | null, sourceApplication?: string | null, message?: string | null, referencedObject: string, createdAt?: any | null, commentCount: number } | null> | null } | null } | null } | null }; +export type StreamWithBranchQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, branch?: { __typename?: 'Branch', id: string, name: string, description?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Commit', id: string, authorName?: string | null, authorId?: string | null, authorAvatar?: string | null, sourceApplication?: string | null, message?: string | null, referencedObject: string, createdAt?: string | null, commentCount: number } | null> | null } | null } | null } | null }; export type BranchCreatedSubscriptionVariables = Exact<{ streamId: Scalars['String']; @@ -1547,7 +1672,7 @@ export type BranchCreatedSubscriptionVariables = Exact<{ export type BranchCreatedSubscription = { __typename?: 'Subscription', branchCreated?: Record | null }; -export type CommentFullInfoFragment = { __typename?: 'Comment', id: string, archived: boolean, authorId: string, data?: Record | null, screenshot?: string | null, createdAt?: any | null, updatedAt?: any | null, viewedAt?: any | null, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null, attachments?: Array<{ __typename?: 'BlobMetadata', id: string, fileName: string, streamId: string, fileType: string, fileSize?: number | null }> | null }, replies?: { __typename?: 'CommentCollection', totalCount: number } | null, resources: Array<{ __typename?: 'ResourceIdentifier', resourceId: string, resourceType: ResourceType } | null> }; +export type CommentFullInfoFragment = { __typename?: 'Comment', id: string, archived: boolean, authorId: string, data?: Record | null, screenshot?: string | null, createdAt?: string | null, updatedAt?: string | null, viewedAt?: string | null, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null, attachments?: Array<{ __typename?: 'BlobMetadata', id: string, fileName: string, streamId: string, fileType: string, fileSize?: number | null }> | null }, replies?: { __typename?: 'CommentCollection', totalCount: number } | null, resources: Array<{ __typename?: 'ResourceIdentifier', resourceId: string, resourceType: ResourceType } | null> }; export type StreamCommitQueryQueryVariables = Exact<{ streamId: Scalars['String']; @@ -1555,11 +1680,17 @@ export type StreamCommitQueryQueryVariables = Exact<{ }>; -export type StreamCommitQueryQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, role?: string | null, commit?: { __typename?: 'Commit', id: string, message?: string | null, referencedObject: string, authorName?: string | null, authorId?: string | null, authorAvatar?: string | null, createdAt?: any | null, branchName?: string | null, sourceApplication?: string | null } | null } | null }; +export type StreamCommitQueryQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, role?: string | null, commit?: { __typename?: 'Commit', id: string, message?: string | null, referencedObject: string, authorName?: string | null, authorId?: string | null, authorAvatar?: string | null, createdAt?: string | null, branchName?: string | null, sourceApplication?: string | null } | null } | null }; -export type ActivityMainFieldsFragment = { __typename?: 'Activity', id: string, actionType: string, info: Record, userId: string, streamId?: string | null, resourceId: string, resourceType: string, time: any, message: string }; +export type BasicStreamAccessRequestFieldsFragment = { __typename?: 'StreamAccessRequest', id: string, streamId: string, createdAt: string }; -export type LimitedCommitActivityFieldsFragment = { __typename?: 'Activity', id: string, info: Record, time: any, userId: string, message: string }; +export type FullStreamAccessRequestFieldsFragment = { __typename?: 'StreamAccessRequest', id: string, streamId: string, createdAt: string, requester: { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null } }; + +export type ActivityMainFieldsFragment = { __typename?: 'Activity', id: string, actionType: string, info: Record, userId: string, streamId?: string | null, resourceId: string, resourceType: string, time: string, message: string }; + +export type LimitedCommitActivityFieldsFragment = { __typename?: 'Activity', id: string, info: Record, time: string, userId: string, message: string }; + +export type StreamPendingAccessRequestsFragment = { __typename?: 'Stream', pendingAccessRequests?: Array<{ __typename?: 'StreamAccessRequest', id: string, streamId: string, createdAt: string, requester: { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null } }> | null }; export type LimitedUserFieldsFragment = { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null }; @@ -1669,30 +1800,30 @@ export type StreamCommitsQueryVariables = Exact<{ }>; -export type StreamCommitsQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, role?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, authorId?: string | null, authorName?: string | null, authorAvatar?: string | null, createdAt?: any | null, message?: string | null, referencedObject: string, branchName?: string | null, sourceApplication?: string | null } | null> | null } | null } | null }; +export type StreamCommitsQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, role?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, authorId?: string | null, authorName?: string | null, authorAvatar?: string | null, createdAt?: string | null, message?: string | null, referencedObject: string, branchName?: string | null, sourceApplication?: string | null } | null> | null } | null } | null }; export type StreamsQueryVariables = Exact<{ cursor?: InputMaybe; }>; -export type StreamsQuery = { __typename?: 'Query', streams?: { __typename?: 'StreamCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: any, updatedAt: any, commentCount: number, favoritedDate?: any | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, createdAt?: any | null, message?: string | null, authorId?: string | null, branchName?: string | null, authorName?: string | null, authorAvatar?: string | null, referencedObject: string } | null> | null } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null }> | null } | null }; +export type StreamsQuery = { __typename?: 'Query', streams?: { __typename?: 'StreamCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: string, updatedAt: string, commentCount: number, favoritedDate?: string | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, createdAt?: string | null, message?: string | null, authorId?: string | null, branchName?: string | null, authorName?: string | null, authorAvatar?: string | null, referencedObject: string } | null> | null } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null }> | null } | null }; -export type CommonStreamFieldsFragment = { __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: any, updatedAt: any, commentCount: number, favoritedDate?: any | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null }; +export type CommonStreamFieldsFragment = { __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: string, updatedAt: string, commentCount: number, favoritedDate?: string | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null }; export type StreamQueryVariables = Exact<{ id: Scalars['String']; }>; -export type StreamQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: any, updatedAt: any, commentCount: number, favoritedDate?: any | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null } | null }; +export type StreamQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: string, updatedAt: string, commentCount: number, favoritedDate?: string | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null } | null }; export type StreamWithCollaboratorsQueryVariables = Exact<{ id: Scalars['String']; }>; -export type StreamWithCollaboratorsQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, isPublic: boolean, role?: string | null, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, role: string, company?: string | null, avatar?: string | null }>, pendingCollaborators?: Array<{ __typename?: 'PendingStreamCollaborator', title: string, inviteId: string, role: string, user?: { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null } | null }> | null } | null }; +export type StreamWithCollaboratorsQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, isPublic: boolean, role?: string | null, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, role: string, company?: string | null, avatar?: string | null }>, pendingCollaborators?: Array<{ __typename?: 'PendingStreamCollaborator', title: string, inviteId: string, role: string, user?: { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null } | null }> | null, pendingAccessRequests?: Array<{ __typename?: 'StreamAccessRequest', id: string, streamId: string, createdAt: string, requester: { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null } }> | null } | null }; export type StreamWithActivityQueryVariables = Exact<{ id: Scalars['String']; @@ -1700,7 +1831,7 @@ export type StreamWithActivityQueryVariables = Exact<{ }>; -export type StreamWithActivityQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, createdAt: any, commits?: { __typename?: 'CommitCollection', totalCount: number } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null, activity?: { __typename?: 'ActivityCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Activity', id: string, actionType: string, info: Record, userId: string, streamId?: string | null, resourceId: string, resourceType: string, time: any, message: string } | null> | null } | null } | null }; +export type StreamWithActivityQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, createdAt: string, commits?: { __typename?: 'CommitCollection', totalCount: number } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null, activity?: { __typename?: 'ActivityCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Activity', id: string, actionType: string, info: Record, userId: string, streamId?: string | null, resourceId: string, resourceType: string, time: string, message: string } | null> | null } | null } | null }; export type LeaveStreamMutationVariables = Exact<{ streamId: Scalars['String']; @@ -1731,24 +1862,24 @@ export type StreamBranchFirstCommitQueryVariables = Exact<{ export type StreamBranchFirstCommitQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, branch?: { __typename?: 'Branch', commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, referencedObject: string } | null> | null } | null } | null } | null }; -export type CommonUserFieldsFragment = { __typename?: 'User', id: string, suuid?: string | null, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, profiles?: Record | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: any | null } | null> | null } | null }; +export type CommonUserFieldsFragment = { __typename?: 'User', id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null, profiles?: Record | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: string | null } | null> | null } | null }; export type UserFavoriteStreamsQueryVariables = Exact<{ cursor?: InputMaybe; }>; -export type UserFavoriteStreamsQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, suuid?: string | null, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, profiles?: Record | null, role?: string | null, favoriteStreams?: { __typename?: 'StreamCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: any, updatedAt: any, commentCount: number, favoritedDate?: any | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null }> | null } | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: any | null } | null> | null } | null } | null }; +export type UserFavoriteStreamsQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null, profiles?: Record | null, role?: string | null, favoriteStreams?: { __typename?: 'StreamCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Stream', id: string, name: string, description?: string | null, role?: string | null, isPublic: boolean, createdAt: string, updatedAt: string, commentCount: number, favoritedDate?: string | null, favoritesCount: number, collaborators: Array<{ __typename?: 'StreamCollaborator', id: string, name: string, company?: string | null, avatar?: string | null, role: string }>, commits?: { __typename?: 'CommitCollection', totalCount: number } | null, branches?: { __typename?: 'BranchCollection', totalCount: number } | null }> | null } | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: string | null } | null> | null } | null } | null }; export type MainUserDataQueryVariables = Exact<{ [key: string]: never; }>; -export type MainUserDataQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, suuid?: string | null, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, profiles?: Record | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: any | null } | null> | null } | null } | null }; +export type MainUserDataQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null, profiles?: Record | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: string | null } | null> | null } | null } | null }; export type ExtraUserDataQueryVariables = Exact<{ [key: string]: never; }>; -export type ExtraUserDataQuery = { __typename?: 'Query', user?: { __typename?: 'User', totalOwnedStreamsFavorites: number, id: string, suuid?: string | null, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, profiles?: Record | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: any | null } | null> | null } | null } | null }; +export type ExtraUserDataQuery = { __typename?: 'Query', user?: { __typename?: 'User', totalOwnedStreamsFavorites: number, id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null, profiles?: Record | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: string | null } | null> | null } | null } | null }; export type UserSearchQueryVariables = Exact<{ query: Scalars['String']; @@ -1772,21 +1903,38 @@ export type AdminUsersListQueryVariables = Exact<{ }>; -export type AdminUsersListQuery = { __typename?: 'Query', adminUsers?: { __typename?: 'AdminUsersListCollection', totalCount: number, items: Array<{ __typename?: 'AdminUsersListItem', id: string, registeredUser?: { __typename?: 'User', id: string, suuid?: string | null, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, profiles?: Record | null, role?: string | null, authorizedApps?: Array<{ __typename?: 'ServerAppListItem', name: string } | null> | null } | null, invitedUser?: { __typename?: 'ServerInvite', id: string, email: string, invitedBy: { __typename?: 'LimitedUser', id: string, name?: string | null } } | null }> } | null }; +export type AdminUsersListQuery = { __typename?: 'Query', adminUsers?: { __typename?: 'AdminUsersListCollection', totalCount: number, items: Array<{ __typename?: 'AdminUsersListItem', id: string, registeredUser?: { __typename?: 'User', id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, profiles?: Record | null, role?: string | null, authorizedApps?: Array<{ __typename?: 'ServerAppListItem', name: string } | null> | null } | null, invitedUser?: { __typename?: 'ServerInvite', id: string, email: string, invitedBy: { __typename?: 'LimitedUser', id: string, name?: string | null } } | null }> } | null }; export type UserTimelineQueryVariables = Exact<{ cursor?: InputMaybe; }>; -export type UserTimelineQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, timeline?: { __typename?: 'ActivityCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Activity', id: string, actionType: string, info: Record, userId: string, streamId?: string | null, resourceId: string, resourceType: string, time: any, message: string } | null> | null } | null } | null }; +export type UserTimelineQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, timeline?: { __typename?: 'ActivityCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Activity', id: string, actionType: string, info: Record, userId: string, streamId?: string | null, resourceId: string, resourceType: string, time: string, message: string } | null> | null } | null } | null }; -export type UserQueryVariables = Exact<{ +export type ValidatePasswordStrengthQueryVariables = Exact<{ + pwd: Scalars['String']; +}>; + + +export type ValidatePasswordStrengthQuery = { __typename?: 'Query', userPwdStrength: { __typename?: 'PasswordStrengthCheckResults', score: number, feedback: { __typename?: 'PasswordStrengthCheckFeedback', warning?: string | null, suggestions: Array } } }; + +export type EmailVerificationBannerStateQueryVariables = Exact<{ [key: string]: never; }>; + + +export type EmailVerificationBannerStateQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email?: string | null, verified?: boolean | null, hasPendingVerification?: boolean | null } | null }; + +export type RequestVerificationMutationVariables = Exact<{ [key: string]: never; }>; + + +export type RequestVerificationMutation = { __typename?: 'Mutation', requestVerification: boolean }; + +export type UserByIdQueryVariables = Exact<{ id: Scalars['String']; }>; -export type UserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, profiles?: Record | null, role?: string | null, suuid?: string | null } | null }; +export type UserByIdQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, profiles?: Record | null, role?: string | null } | null }; export type UserProfileQueryVariables = Exact<{ id: Scalars['String']; @@ -1808,7 +1956,7 @@ export type WebhooksQueryVariables = Exact<{ }>; -export type WebhooksQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, role?: string | null, webhooks?: { __typename?: 'WebhookCollection', items?: Array<{ __typename?: 'Webhook', id: string, streamId: string, url: string, description?: string | null, triggers: Array, enabled?: boolean | null, history?: { __typename?: 'WebhookEventCollection', items?: Array<{ __typename?: 'WebhookEvent', status: number, statusInfo: string, lastUpdate: any } | null> | null } | null } | null> | null } | null } | null }; +export type WebhooksQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, role?: string | null, webhooks?: { __typename?: 'WebhookCollection', items?: Array<{ __typename?: 'Webhook', id: string, streamId: string, url: string, description?: string | null, triggers: Array, enabled?: boolean | null, history?: { __typename?: 'WebhookEventCollection', items?: Array<{ __typename?: 'WebhookEvent', status: number, statusInfo: string, lastUpdate: string } | null> | null } | null } | null> | null } | null } | null }; export const CommentFullInfo = gql` fragment CommentFullInfo on Comment { @@ -1861,13 +2009,11 @@ export const LimitedCommitActivityFields = gql` message } `; -export const StreamCollaboratorFields = gql` - fragment StreamCollaboratorFields on StreamCollaborator { +export const BasicStreamAccessRequestFields = gql` + fragment BasicStreamAccessRequestFields on StreamAccessRequest { id - name - role - company - avatar + streamId + createdAt } `; export const LimitedUserFields = gql` @@ -1880,6 +2026,30 @@ export const LimitedUserFields = gql` verified } `; +export const FullStreamAccessRequestFields = gql` + fragment FullStreamAccessRequestFields on StreamAccessRequest { + ...BasicStreamAccessRequestFields + requester { + ...LimitedUserFields + } +} + `; +export const StreamPendingAccessRequests = gql` + fragment StreamPendingAccessRequests on Stream { + pendingAccessRequests { + ...FullStreamAccessRequestFields + } +} + `; +export const StreamCollaboratorFields = gql` + fragment StreamCollaboratorFields on StreamCollaborator { + id + name + role + company + avatar +} + `; export const UsersOwnInviteFields = gql` fragment UsersOwnInviteFields on PendingStreamCollaborator { id @@ -1891,7 +2061,7 @@ export const UsersOwnInviteFields = gql` ...LimitedUserFields } } - ${LimitedUserFields}`; + `; export const ServerInfoBlobSizeFields = gql` fragment ServerInfoBlobSizeFields on ServerInfo { blobSizeLimitBytes @@ -1956,16 +2126,15 @@ export const CommonStreamFields = gql` export const CommonUserFields = gql` fragment CommonUserFields on User { id - suuid email name bio company avatar verified + hasPendingVerification profiles role - suuid streams { totalCount } @@ -1978,6 +2147,25 @@ export const CommonUserFields = gql` } } `; +export const GetStreamAccessRequest = gql` + query GetStreamAccessRequest($streamId: String!) { + streamAccessRequest(streamId: $streamId) { + ...BasicStreamAccessRequestFields + } +} + ${BasicStreamAccessRequestFields}`; +export const CreateStreamAccessRequest = gql` + mutation CreateStreamAccessRequest($streamId: String!) { + streamAccessRequestCreate(streamId: $streamId) { + ...BasicStreamAccessRequestFields + } +} + ${BasicStreamAccessRequestFields}`; +export const UseStreamAccessRequest = gql` + mutation UseStreamAccessRequest($requestId: String!, $accept: Boolean!, $role: StreamRole = STREAM_CONTRIBUTOR) { + streamAccessRequestUse(requestId: $requestId, accept: $accept, role: $role) +} + `; export const StreamWithBranch = gql` query StreamWithBranch($streamId: String!, $branchName: String!, $cursor: String) { stream(id: $streamId) { @@ -2037,14 +2225,16 @@ export const StreamInvite = gql` ...UsersOwnInviteFields } } - ${UsersOwnInviteFields}`; + ${UsersOwnInviteFields} +${LimitedUserFields}`; export const UserStreamInvites = gql` query UserStreamInvites { streamInvites { ...UsersOwnInviteFields } } - ${UsersOwnInviteFields}`; + ${UsersOwnInviteFields} +${LimitedUserFields}`; export const UseStreamInvite = gql` mutation UseStreamInvite($accept: Boolean!, $streamId: String!, $token: String!) { streamInviteUse(accept: $accept, streamId: $streamId, token: $token) @@ -2218,10 +2408,15 @@ export const StreamWithCollaborators = gql` ...LimitedUserFields } } + pendingAccessRequests { + ...FullStreamAccessRequestFields + } } } ${StreamCollaboratorFields} -${LimitedUserFields}`; +${LimitedUserFields} +${FullStreamAccessRequestFields} +${BasicStreamAccessRequestFields}`; export const StreamWithActivity = gql` query StreamWithActivity($id: String!, $cursor: DateTime) { stream(id: $id) { @@ -2339,7 +2534,6 @@ export const AdminUsersList = gql` id registeredUser { id - suuid email name bio @@ -2378,8 +2572,34 @@ export const UserTimeline = gql` } } ${ActivityMainFields}`; -export const User = gql` - query User($id: String!) { +export const ValidatePasswordStrength = gql` + query ValidatePasswordStrength($pwd: String!) { + userPwdStrength(pwd: $pwd) { + score + feedback { + warning + suggestions + } + } +} + `; +export const EmailVerificationBannerState = gql` + query EmailVerificationBannerState { + user { + id + email + verified + hasPendingVerification + } +} + `; +export const RequestVerification = gql` + mutation RequestVerification { + requestVerification +} + `; +export const UserById = gql` + query UserById($id: String!) { user(id: $id) { id email @@ -2390,7 +2610,6 @@ export const User = gql` verified profiles role - suuid } } `; @@ -2459,20 +2678,26 @@ export const Webhooks = gql` export const CommentFullInfoFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CommentFullInfo"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"archived"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}},{"kind":"Field","name":{"kind":"Name","value":"attachments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fileName"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"fileType"}},{"kind":"Field","name":{"kind":"Name","value":"fileSize"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"replies"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"resources"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resourceId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"viewedAt"}}]}}]} as unknown as DocumentNode; export const ActivityMainFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ActivityMainFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Activity"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"actionType"}},{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceType"}},{"kind":"Field","name":{"kind":"Name","value":"time"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]} as unknown as DocumentNode; export const LimitedCommitActivityFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedCommitActivityFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Activity"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"time"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]} as unknown as DocumentNode; -export const StreamCollaboratorFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"StreamCollaboratorFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]} as unknown as DocumentNode; +export const BasicStreamAccessRequestFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const LimitedUserFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}}]}}]} as unknown as DocumentNode; -export const UsersOwnInviteFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UsersOwnInviteFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingStreamCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"streamName"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserFields"}}]}}]}},...LimitedUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode; +export const FullStreamAccessRequestFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserFields"}}]}}]}}]} as unknown as DocumentNode; +export const StreamPendingAccessRequestsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"StreamPendingAccessRequests"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Stream"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pendingAccessRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullStreamAccessRequestFields"}}]}}]}}]} as unknown as DocumentNode; +export const StreamCollaboratorFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"StreamCollaboratorFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]} as unknown as DocumentNode; +export const UsersOwnInviteFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UsersOwnInviteFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingStreamCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"streamName"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserFields"}}]}}]}}]} as unknown as DocumentNode; export const ServerInfoBlobSizeFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerInfoBlobSizeFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"blobSizeLimitBytes"}}]}}]} as unknown as DocumentNode; export const MainServerInfoFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MainServerInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"adminContact"}},{"kind":"Field","name":{"kind":"Name","value":"canonicalUrl"}},{"kind":"Field","name":{"kind":"Name","value":"termsOfService"}},{"kind":"Field","name":{"kind":"Name","value":"inviteOnly"}},{"kind":"Field","name":{"kind":"Name","value":"version"}}]}}]} as unknown as DocumentNode; export const ServerInfoRolesFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerInfoRolesFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"roles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"resourceTarget"}}]}}]}}]} as unknown as DocumentNode; export const ServerInfoScopesFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerInfoScopesFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scopes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]} as unknown as DocumentNode; export const CommonStreamFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CommonStreamFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Stream"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"isPublic"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"commentCount"}},{"kind":"Field","name":{"kind":"Name","value":"collaborators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"branches"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"favoritedDate"}},{"kind":"Field","name":{"kind":"Name","value":"favoritesCount"}}]}}]} as unknown as DocumentNode; -export const CommonUserFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CommonUserFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"suuid"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"profiles"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"suuid"}},{"kind":"Field","name":{"kind":"Name","value":"streams"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CommonUserFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CommonUserFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"hasPendingVerification"}},{"kind":"Field","name":{"kind":"Name","value":"profiles"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"streams"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}}]}}]}},...BasicStreamAccessRequestFieldsFragmentDoc.definitions]} as unknown as DocumentNode; +export const CreateStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequestCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}}]}}]}},...BasicStreamAccessRequestFieldsFragmentDoc.definitions]} as unknown as DocumentNode; +export const UseStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UseStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"requestId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"accept"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"role"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"StreamRole"}},"defaultValue":{"kind":"EnumValue","value":"STREAM_CONTRIBUTOR"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequestUse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"requestId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"requestId"}}},{"kind":"Argument","name":{"kind":"Name","value":"accept"},"value":{"kind":"Variable","name":{"kind":"Name","value":"accept"}}},{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"role"}}}]}]}}]} as unknown as DocumentNode; export const StreamWithBranchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamWithBranch"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"branchName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"branch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"branchName"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"4"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"authorName"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"authorAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"commentCount"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const BranchCreatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"BranchCreated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"branchCreated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}]}]}}]} as unknown as DocumentNode; export const StreamCommitQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamCommitQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"commit"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"authorName"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"authorAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"branchName"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}}]}}]}}]}}]} as unknown as DocumentNode; -export const StreamInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamInvite"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UsersOwnInviteFields"}}]}}]}},...UsersOwnInviteFieldsFragmentDoc.definitions]} as unknown as DocumentNode; -export const UserStreamInvitesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserStreamInvites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamInvites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UsersOwnInviteFields"}}]}}]}},...UsersOwnInviteFieldsFragmentDoc.definitions]} as unknown as DocumentNode; +export const StreamInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamInvite"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UsersOwnInviteFields"}}]}}]}},...UsersOwnInviteFieldsFragmentDoc.definitions,...LimitedUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode; +export const UserStreamInvitesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserStreamInvites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamInvites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UsersOwnInviteFields"}}]}}]}},...UsersOwnInviteFieldsFragmentDoc.definitions,...LimitedUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode; export const UseStreamInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UseStreamInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"accept"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamInviteUse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"accept"},"value":{"kind":"Variable","name":{"kind":"Name","value":"accept"}}},{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}]}]}}]} as unknown as DocumentNode; export const CancelStreamInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CancelStreamInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamInviteCancel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}},{"kind":"Argument","name":{"kind":"Name","value":"inviteId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}}}]}]}}]} as unknown as DocumentNode; export const DeleteInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inviteId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}}}]}]}}]} as unknown as DocumentNode; @@ -2487,7 +2712,7 @@ export const ServerInfoBlobSizeLimitDocument = {"kind":"Document","definitions": export const StreamCommitsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamCommits"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"authorName"}},{"kind":"Field","name":{"kind":"Name","value":"authorAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"branchName"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const StreamsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Streams"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streams"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"isPublic"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"commentCount"}},{"kind":"Field","name":{"kind":"Name","value":"collaborators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"branchName"}},{"kind":"Field","name":{"kind":"Name","value":"authorName"}},{"kind":"Field","name":{"kind":"Name","value":"authorAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"branches"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"favoritedDate"}},{"kind":"Field","name":{"kind":"Name","value":"favoritesCount"}}]}}]}}]}}]} as unknown as DocumentNode; export const StreamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Stream"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonStreamFields"}}]}}]}},...CommonStreamFieldsFragmentDoc.definitions]} as unknown as DocumentNode; -export const StreamWithCollaboratorsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamWithCollaborators"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isPublic"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"collaborators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"StreamCollaboratorFields"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pendingCollaborators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserFields"}}]}}]}}]}}]}},...StreamCollaboratorFieldsFragmentDoc.definitions,...LimitedUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode; +export const StreamWithCollaboratorsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamWithCollaborators"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isPublic"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"collaborators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"StreamCollaboratorFields"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pendingCollaborators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserFields"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pendingAccessRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullStreamAccessRequestFields"}}]}}]}}]}},...StreamCollaboratorFieldsFragmentDoc.definitions,...LimitedUserFieldsFragmentDoc.definitions,...FullStreamAccessRequestFieldsFragmentDoc.definitions,...BasicStreamAccessRequestFieldsFragmentDoc.definitions]} as unknown as DocumentNode; export const StreamWithActivityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamWithActivity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"branches"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"activity"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ActivityMainFields"}}]}}]}}]}}]}},...ActivityMainFieldsFragmentDoc.definitions]} as unknown as DocumentNode; export const LeaveStreamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"LeaveStream"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamLeave"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}]}]}}]} as unknown as DocumentNode; export const UpdateStreamPermissionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateStreamPermission"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"params"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"StreamUpdatePermissionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamUpdatePermission"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"permissionParams"},"value":{"kind":"Variable","name":{"kind":"Name","value":"params"}}}]}]}}]} as unknown as DocumentNode; @@ -2498,9 +2723,12 @@ export const MainUserDataDocument = {"kind":"Document","definitions":[{"kind":"O export const ExtraUserDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ExtraUserData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonUserFields"}},{"kind":"Field","name":{"kind":"Name","value":"totalOwnedStreamsFavorites"}}]}}]}},...CommonUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode; export const UserSearchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserSearch"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"query"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"archived"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userSearch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"archived"},"value":{"kind":"Variable","name":{"kind":"Name","value":"archived"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserFields"}}]}}]}}]}},...LimitedUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode; export const IsLoggedInDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"IsLoggedIn"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; -export const AdminUsersListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminUsersList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"query"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminUsers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}},{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"registeredUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"suuid"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"profiles"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"authorizedApps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"invitedUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const AdminUsersListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminUsersList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"query"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminUsers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}},{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"registeredUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"profiles"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"authorizedApps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"invitedUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const UserTimelineDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserTimeline"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"timeline"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ActivityMainFields"}}]}}]}}]}}]}},...ActivityMainFieldsFragmentDoc.definitions]} as unknown as DocumentNode; -export const UserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"User"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"profiles"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"suuid"}}]}}]}}]} as unknown as DocumentNode; +export const ValidatePasswordStrengthDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ValidatePasswordStrength"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pwd"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userPwdStrength"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pwd"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pwd"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"feedback"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"suggestions"}}]}}]}}]}}]} as unknown as DocumentNode; +export const EmailVerificationBannerStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EmailVerificationBannerState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"hasPendingVerification"}}]}}]}}]} as unknown as DocumentNode; +export const RequestVerificationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RequestVerification"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"requestVerification"}}]}}]} as unknown as DocumentNode; +export const UserByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"profiles"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]} as unknown as DocumentNode; export const UserProfileDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserProfile"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}}]}}]}}]} as unknown as DocumentNode; export const WebhookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"webhook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"webhookId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"webhooks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"webhookId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"triggers"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"history"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"statusInfo"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const WebhooksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"webhooks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"webhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"triggers"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"history"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"50"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"statusInfo"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdate"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/packages/frontend/src/graphql/streams.js b/packages/frontend/src/graphql/streams.js index 8ca8c6cd6..443e213cd 100644 --- a/packages/frontend/src/graphql/streams.js +++ b/packages/frontend/src/graphql/streams.js @@ -1,3 +1,4 @@ +import { fullStreamAccessRequestFieldsFragment } from '@/graphql/fragments/accessRequests' import { activityMainFieldsFragment } from '@/graphql/fragments/activity' import { limitedUserFieldsFragment, @@ -70,10 +71,14 @@ export const streamWithCollaboratorsQuery = gql` ...LimitedUserFields } } + pendingAccessRequests { + ...FullStreamAccessRequestFields + } } } ${limitedUserFieldsFragment} ${streamCollaboratorFieldsFragment} + ${fullStreamAccessRequestFieldsFragment} ` export const streamWithActivityQuery = gql` diff --git a/packages/frontend/src/graphql/user.js b/packages/frontend/src/graphql/user.js index 7031d6569..a43b93074 100644 --- a/packages/frontend/src/graphql/user.js +++ b/packages/frontend/src/graphql/user.js @@ -6,16 +6,15 @@ import { gql } from '@apollo/client/core' export const commonUserFieldsFragment = gql` fragment CommonUserFields on User { id - suuid email name bio company avatar verified + hasPendingVerification profiles role - suuid streams { totalCount } @@ -115,7 +114,6 @@ export const adminUsersListQuery = gql` id registeredUser { id - suuid email name bio @@ -157,3 +155,32 @@ export const userTimelineQuery = gql` ${activityMainFieldsFragment} ` + +export const validatePasswordStrengthQuery = gql` + query ValidatePasswordStrength($pwd: String!) { + userPwdStrength(pwd: $pwd) { + score + feedback { + warning + suggestions + } + } + } +` + +export const emailVerificationBannerStateQuery = gql` + query EmailVerificationBannerState { + user { + id + email + verified + hasPendingVerification + } + } +` + +export const requestVerificationMutation = gql` + mutation RequestVerification { + requestVerification + } +` diff --git a/packages/frontend/src/graphql/userById.gql b/packages/frontend/src/graphql/userById.gql index a1efe7a90..cde75676b 100644 --- a/packages/frontend/src/graphql/userById.gql +++ b/packages/frontend/src/graphql/userById.gql @@ -1,4 +1,4 @@ -query User($id: String!) { +query UserById($id: String!) { user(id: $id) { id email @@ -9,6 +9,5 @@ query User($id: String!) { verified profiles role - suuid } } diff --git a/packages/frontend/src/helpers/mainConstants.js b/packages/frontend/src/helpers/mainConstants.js deleted file mode 100644 index 7670c98fd..000000000 --- a/packages/frontend/src/helpers/mainConstants.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Speckle role constants - */ -export const Roles = Object.freeze({ - Stream: { - Owner: 'stream:owner', - Contributor: 'stream:contributor', - Reviewer: 'stream:reviewer' - }, - Server: { - Admin: 'server:admin', - User: 'server:user', - ArchivedUser: 'server:archived-user' - } -}) - -/** - * Keys for values stored in localStorage - */ -export const LocalStorageKeys = Object.freeze({ - AuthToken: 'AuthToken', - RefreshToken: 'RefreshToken', - Uuid: 'uuid', - ShouldRedirectTo: 'shouldRedirectTo' -}) diff --git a/packages/frontend/src/helpers/mainConstants.ts b/packages/frontend/src/helpers/mainConstants.ts new file mode 100644 index 000000000..96699a7e9 --- /dev/null +++ b/packages/frontend/src/helpers/mainConstants.ts @@ -0,0 +1,46 @@ +import { StreamRole } from '@/graphql/generated/graphql' + +/** + * Speckle role constants + */ +export const Roles = Object.freeze({ + Stream: { + Owner: 'stream:owner', + Contributor: 'stream:contributor', + Reviewer: 'stream:reviewer' + }, + Server: { + Admin: 'server:admin', + User: 'server:user', + ArchivedUser: 'server:archived-user' + } +}) + +export type ServerRoles = typeof Roles['Server'][keyof typeof Roles['Server']] +export type StreamRoles = typeof Roles['Stream'][keyof typeof Roles['Stream']] + +/** + * Keys for values stored in localStorage + */ +export const LocalStorageKeys = Object.freeze({ + AuthToken: 'AuthToken', + RefreshToken: 'RefreshToken', + Uuid: 'uuid', + ShouldRedirectTo: 'shouldRedirectTo' +}) + +/** + * Our GQL schema has a StreamRoles enum that unfortunately can't have the same exact values as our roles constants, because + * we can't use colons (:) there. So you can use this function to map from our constant value to the GQL one. + */ +export function streamRoleToGraphQLEnum(role: StreamRoles): StreamRole { + switch (role) { + case Roles.Stream.Owner: + return StreamRole.StreamOwner + case Roles.Stream.Reviewer: + return StreamRole.StreamReviewer + case Roles.Stream.Contributor: + default: + return StreamRole.StreamContributor + } +} diff --git a/packages/frontend/src/helpers/typeHelpers.ts b/packages/frontend/src/helpers/typeHelpers.ts index 5744b125b..bdc679904 100644 --- a/packages/frontend/src/helpers/typeHelpers.ts +++ b/packages/frontend/src/helpers/typeHelpers.ts @@ -1,4 +1,5 @@ import { ReactiveVar } from '@apollo/client/core' +import { isUndefined } from 'lodash' import Vue, { VueConstructor } from 'vue' export type Nullable = T | null @@ -7,6 +8,16 @@ export type Optional = T | undefined export type MaybeFalsy = T | null | undefined | false | '' | 0 +export type MaybeNullOrUndefined = T | null | undefined + +export type MaybeAsync = T | Promise + +/** + * In TS undefined !== void, so use this type guard to check for both + */ +export const isUndefinedOrVoid = (val: unknown): val is void | undefined => + isUndefined(val) + // eslint-disable-next-line @typescript-eslint/no-explicit-any export type GetReactiveVarType> = V extends ReactiveVar< infer T diff --git a/packages/frontend/src/helpers/vuetifyHelpers.ts b/packages/frontend/src/helpers/vuetifyHelpers.ts index 74c0bb3e6..aeb2fb7d7 100644 --- a/packages/frontend/src/helpers/vuetifyHelpers.ts +++ b/packages/frontend/src/helpers/vuetifyHelpers.ts @@ -17,5 +17,6 @@ export type VFormInstance = CombinedVueInstance< validate(): boolean }, unknown, + unknown, unknown > diff --git a/packages/frontend/src/main/app.js b/packages/frontend/src/main/app.js index acf83a013..a0116b78e 100644 --- a/packages/frontend/src/main/app.js +++ b/packages/frontend/src/main/app.js @@ -10,7 +10,7 @@ import { DefaultApolloClient } from '@vue/apollo-composable' import { createProvider, installVueApollo } from '@/config/apolloConfig' import { checkAccessCodeAndGetTokens, - prefetchUserAndSetSuuid + prefetchUserAndSetID } from '@/plugins/authHelpers' import router from '@/main/router/index' @@ -58,7 +58,7 @@ installVueApollo(apolloProvider) // TODO: Sort out error handling here, if something goes wrong it just goes into an infinite loop if (AuthToken) { - prefetchUserAndSetSuuid(apolloProvider.defaultClient) + prefetchUserAndSetID(apolloProvider.defaultClient) .then(() => { postAuthInit() }) @@ -72,7 +72,7 @@ if (AuthToken) { } else { checkAccessCodeAndGetTokens() .then(() => { - return prefetchUserAndSetSuuid(apolloProvider.defaultClient) + return prefetchUserAndSetID(apolloProvider.defaultClient) }) .then(() => { postAuthInit() diff --git a/packages/frontend/src/main/components/activity/ListItemActivityDescription.vue b/packages/frontend/src/main/components/activity/ListItemActivityDescription.vue index 580d50c6f..b7fc09849 100644 --- a/packages/frontend/src/main/components/activity/ListItemActivityDescription.vue +++ b/packages/frontend/src/main/components/activity/ListItemActivityDescription.vue @@ -46,6 +46,12 @@ {{ val ? 'public' : 'private' }} +

@@ -61,7 +67,8 @@ const UpdatedInfoKeys = { Name: 'name', Description: 'description', Message: 'message', - IsPublic: 'isPublic' + IsPublic: 'isPublic', + IsDiscoverable: 'isDiscoverable' } export default { diff --git a/packages/frontend/src/main/components/auth/AuthStrategies.vue b/packages/frontend/src/main/components/auth/AuthStrategies.vue index ffb51073b..753b3fa59 100644 --- a/packages/frontend/src/main/components/auth/AuthStrategies.vue +++ b/packages/frontend/src/main/components/auth/AuthStrategies.vue @@ -18,8 +18,8 @@ block :color="s.color" :href="`${s.url}?appId=${appId}&challenge=${challenge}${ - suuid ? '&suuid=' + suuid : '' - }${token ? '&token=' + token : ''}`" + token ? '&token=' + token : '' + }`" > {{ s.icon }} {{ s.name }} @@ -45,10 +45,6 @@ export default { challenge: { type: String, default: () => null - }, - suuid: { - type: String, - default: () => null } }, computed: { diff --git a/packages/frontend/src/main/components/comments/CommentThreadReplyAttachments.vue b/packages/frontend/src/main/components/comments/CommentThreadReplyAttachments.vue index 1cd782bd9..b2059a0be 100644 --- a/packages/frontend/src/main/components/comments/CommentThreadReplyAttachments.vue +++ b/packages/frontend/src/main/components/comments/CommentThreadReplyAttachments.vue @@ -3,7 +3,7 @@
{ return { showAttachmentPreview: false, - selectedAttachment: null + selectedAttachment: null as Nullable } }, methods: { diff --git a/packages/frontend/src/main/components/common/GlobalToast.vue b/packages/frontend/src/main/components/common/GlobalToast.vue index 1c88f906b..b29c5318f 100644 --- a/packages/frontend/src/main/components/common/GlobalToast.vue +++ b/packages/frontend/src/main/components/common/GlobalToast.vue @@ -12,47 +12,16 @@ diff --git a/packages/frontend/src/main/components/common/NoDataPlaceholder.vue b/packages/frontend/src/main/components/common/NoDataPlaceholder.vue index 00c1a9753..1d9629d1b 100644 --- a/packages/frontend/src/main/components/common/NoDataPlaceholder.vue +++ b/packages/frontend/src/main/components/common/NoDataPlaceholder.vue @@ -26,7 +26,8 @@ link class="primary mb-4" dark - @click="downloadManager" + href="https://releases.speckle.systems/" + target="_blank" > mdi-download @@ -183,23 +184,6 @@ export default { }, beforeDestroy() { clearInterval(this.checkAccountTimer) - }, - methods: { - async downloadManager() { - this.$mixpanel.track('Manager Download', { - type: 'action' - }) - - const url = `https://releases.speckle.dev/manager/SpeckleManager Setup.exe` - - const a = document.createElement('a') - document.body.appendChild(a) - a.style = 'display: none' - a.href = url - a.download = 'SpeckleManager Setup.exe' - a.click() - document.body.removeChild(a) - } } } diff --git a/packages/frontend/src/main/components/common/PreviewImage.vue b/packages/frontend/src/main/components/common/PreviewImage.vue index 3da0b5e50..2e2b6fd87 100644 --- a/packages/frontend/src/main/components/common/PreviewImage.vue +++ b/packages/frontend/src/main/components/common/PreviewImage.vue @@ -49,7 +49,7 @@ export default { props: { url: { type: String, - default: '' + default: () => '' }, color: { type: Boolean, diff --git a/packages/frontend/src/main/components/common/UserAvatar.vue b/packages/frontend/src/main/components/common/UserAvatar.vue index 51f78a88a..7d2ef1ee6 100644 --- a/packages/frontend/src/main/components/common/UserAvatar.vue +++ b/packages/frontend/src/main/components/common/UserAvatar.vue @@ -69,18 +69,27 @@ >
- diff --git a/packages/frontend/src/main/components/common/UserAvatarIcon.vue b/packages/frontend/src/main/components/common/UserAvatarIcon.vue index 22153ffcb..c4bda1fb4 100644 --- a/packages/frontend/src/main/components/common/UserAvatarIcon.vue +++ b/packages/frontend/src/main/components/common/UserAvatarIcon.vue @@ -17,6 +17,9 @@ export default { required: true }, avatar: { + /** + * @type {import('vue').PropType} + */ type: String, default: null } diff --git a/packages/frontend/src/main/components/common/layout/BasicPanel.vue b/packages/frontend/src/main/components/common/layout/BasicPanel.vue new file mode 100644 index 000000000..82fc731cf --- /dev/null +++ b/packages/frontend/src/main/components/common/layout/BasicPanel.vue @@ -0,0 +1,3 @@ + diff --git a/packages/frontend/src/main/components/common/layout/RoundedButtonList.vue b/packages/frontend/src/main/components/common/layout/RoundedButtonList.vue new file mode 100644 index 000000000..f9558d0de --- /dev/null +++ b/packages/frontend/src/main/components/common/layout/RoundedButtonList.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/frontend/src/main/components/common/layout/rounded-button-list/RoundedButtonListItem.vue b/packages/frontend/src/main/components/common/layout/rounded-button-list/RoundedButtonListItem.vue new file mode 100644 index 000000000..77e1723db --- /dev/null +++ b/packages/frontend/src/main/components/common/layout/rounded-button-list/RoundedButtonListItem.vue @@ -0,0 +1,69 @@ + + diff --git a/packages/frontend/src/main/components/feed/FeedTimeline.vue b/packages/frontend/src/main/components/feed/FeedTimeline.vue index e44440a51..b5170fd81 100644 --- a/packages/frontend/src/main/components/feed/FeedTimeline.vue +++ b/packages/frontend/src/main/components/feed/FeedTimeline.vue @@ -82,6 +82,7 @@ import { UserTimelineDocument } from '@/graphql/generated/graphql' import { useQuery } from '@vue/apollo-composable' import { computed } from 'vue' import { AppLocalStorage } from '@/utils/localStorage' +import { SKIPPABLE_ACTION_TYPES } from '@/main/lib/feed/helpers/activityStream' export default { name: 'FeedTimeline', @@ -112,7 +113,7 @@ export default { const data = timelineResult.value if (!data) return [] - const skippableActionTypes = ['stream_invite_sent', 'stream_invite_declined'] + const skippableActionTypes = SKIPPABLE_ACTION_TYPES const groupedTimeline = data.user.timeline.items.reduce(function (prev, curr) { if (skippableActionTypes.includes(curr.actionType)) { return prev diff --git a/packages/frontend/src/main/components/feed/LatestBlogposts.vue b/packages/frontend/src/main/components/feed/LatestBlogposts.vue index 39a0f6b26..9e0d8dbe1 100644 --- a/packages/frontend/src/main/components/feed/LatestBlogposts.vue +++ b/packages/frontend/src/main/components/feed/LatestBlogposts.vue @@ -1,11 +1,7 @@ + diff --git a/packages/frontend/src/main/components/stream/StreamAccessRequestBanner.vue b/packages/frontend/src/main/components/stream/StreamAccessRequestBanner.vue new file mode 100644 index 000000000..1e2187322 --- /dev/null +++ b/packages/frontend/src/main/components/stream/StreamAccessRequestBanner.vue @@ -0,0 +1,176 @@ + + + diff --git a/packages/frontend/src/main/components/stream/StreamActivity.vue b/packages/frontend/src/main/components/stream/StreamActivity.vue index d93abd99f..7f150118f 100644 --- a/packages/frontend/src/main/components/stream/StreamActivity.vue +++ b/packages/frontend/src/main/components/stream/StreamActivity.vue @@ -41,6 +41,7 @@ import { StreamWithActivityDocument } from '@/graphql/generated/graphql' import { useQuery } from '@vue/apollo-composable' import { useRoute } from '@/main/lib/core/composables/router' import { computed } from 'vue' +import { SKIPPABLE_ACTION_TYPES } from '@/main/lib/feed/helpers/activityStream' export default { name: 'StreamActivity', @@ -61,7 +62,7 @@ export default { })) const stream = computed(() => result.value?.stream || null) - const skippableActionTypes = ['stream_invite_sent', 'stream_invite_declined'] + const skippableActionTypes = SKIPPABLE_ACTION_TYPES const groupedActivity = computed(() => (stream.value?.activity?.items || []).reduce(function (prev, curr) { if (skippableActionTypes.includes(curr.actionType)) { diff --git a/packages/frontend/src/main/components/stream/editor/StreamVisibilityToggle.vue b/packages/frontend/src/main/components/stream/editor/StreamVisibilityToggle.vue new file mode 100644 index 000000000..1f3b70f74 --- /dev/null +++ b/packages/frontend/src/main/components/stream/editor/StreamVisibilityToggle.vue @@ -0,0 +1,80 @@ + + + diff --git a/packages/frontend/src/main/components/user/EmailVerificationBanner.vue b/packages/frontend/src/main/components/user/EmailVerificationBanner.vue index 2432238bf..3da8d0e82 100644 --- a/packages/frontend/src/main/components/user/EmailVerificationBanner.vue +++ b/packages/frontend/src/main/components/user/EmailVerificationBanner.vue @@ -1,7 +1,7 @@ - diff --git a/packages/frontend/src/main/components/user/UserInfoCard.vue b/packages/frontend/src/main/components/user/UserInfoCard.vue index f63bae17a..76fd0ab1d 100644 --- a/packages/frontend/src/main/components/user/UserInfoCard.vue +++ b/packages/frontend/src/main/components/user/UserInfoCard.vue @@ -64,8 +64,6 @@ id: {{ user.id }} - , suuid: - {{ user.suuid }}
diff --git a/packages/frontend/src/main/dialogs/BranchEditDialog.vue b/packages/frontend/src/main/dialogs/BranchEditDialog.vue index a5e9c7d16..177b10539 100644 --- a/packages/frontend/src/main/dialogs/BranchEditDialog.vue +++ b/packages/frontend/src/main/dialogs/BranchEditDialog.vue @@ -11,7 +11,7 @@ {{ error }} - + - -

Invite collaborators

@@ -100,7 +96,7 @@ color="primary" block large - :disabled="!valid" + :disabled="!valid || isLoading" :loading="isLoading" elevation="0" type="submit" @@ -115,10 +111,13 @@ import { gql } from '@apollo/client/core' import { userSearchQuery } from '@/graphql/user' import { AppLocalStorage } from '@/utils/localStorage' +import StreamVisibilityToggle from '@/main/components/stream/editor/StreamVisibilityToggle.vue' +import UserAvatar from '@/main/components/common/UserAvatar.vue' export default { components: { - UserAvatar: () => import('@/main/components/common/UserAvatar') + UserAvatar, + StreamVisibilityToggle }, props: { open: { @@ -156,6 +155,7 @@ export default { search: null, nameRules: [], isPublic: true, + isDiscoverable: false, collabs: [], isLoading: false, users: null @@ -210,6 +210,7 @@ export default { myStream: { name: this.name, isPublic: this.isPublic, + isDiscoverable: this.isDiscoverable, description: this.description, withContributors: collabIds } @@ -223,8 +224,9 @@ export default { this.$eventHub.$emit('notification', { text: e.message }) + } finally { + this.isLoading = false } - this.isLoading = false } } } diff --git a/packages/frontend/src/main/layouts/TheMain.vue b/packages/frontend/src/main/layouts/TheMain.vue index cc0666796..1e45613aa 100644 --- a/packages/frontend/src/main/layouts/TheMain.vue +++ b/packages/frontend/src/main/layouts/TheMain.vue @@ -46,8 +46,7 @@ @@ -62,9 +61,9 @@ ` - let emailParams - mailerMock.enable() - mailerMock.mockFunction('sendEmail', (params) => { - emailParams = params - }) + const sendEmailInvocations = mailerMock.hijackFunction( + 'sendEmail', + async () => true + ) const result = await createInvite({ email: targetEmail, @@ -176,6 +163,7 @@ describe('[Stream & Server Invites]', () => { expect(result.errors).to.be.not.ok // Check that email was sent out + const emailParams = sendEmailInvocations.args[0][0] expect(emailParams).to.be.ok expect(emailParams.to).to.eq(targetEmail) expect(emailParams.subject).to.be.ok @@ -307,11 +295,10 @@ describe('[Stream & Server Invites]', () => { const unsanitaryMessage = `
${messagePart1} ` const targetEmail = email || user.email - let emailParams - mailerMock.enable() - mailerMock.mockFunction('sendEmail', (params) => { - emailParams = params - }) + const sendEmailInvocations = mailerMock.hijackFunction( + 'sendEmail', + async () => true + ) const result = await createInvite({ email, @@ -326,6 +313,7 @@ describe('[Stream & Server Invites]', () => { expect(result.errors).to.be.not.ok // Check that email was sent out + const emailParams = sendEmailInvocations.args[0][0] expect(emailParams).to.be.ok expect(emailParams.to).to.eq(targetEmail) expect(emailParams.subject).to.be.ok @@ -429,11 +417,11 @@ describe('[Stream & Server Invites]', () => { }) it('they can resend pre-existing invites irregardless of type', async () => { - const emailParamsArr = [] - mailerMock.enable() - mailerMock.mockFunction('sendEmail', (params) => { - emailParamsArr.push(params) - }) + const sendEmailInvocations = mailerMock.hijackFunction( + 'sendEmail', + async () => true, + { times: invites.length } + ) const inviteIds = invites.map((i) => i.inviteId) @@ -446,7 +434,7 @@ describe('[Stream & Server Invites]', () => { expect(result.errors).to.not.be.ok } - expect(emailParamsArr).to.have.length(inviteIds.length) + expect(sendEmailInvocations.length()).to.eq(inviteIds.length) }) it('they can delete pre-existing invites irregardless of type', async () => { @@ -493,11 +481,11 @@ describe('[Stream & Server Invites]', () => { const emails = ['abababa1@mail.com', 'abababa2@mail.com', 'abababa3@mail.com'] const message = 'ayyoyoyoyoy' - const emailParamsArr = [] - mailerMock.enable() - mailerMock.mockFunction('sendEmail', (params) => { - emailParamsArr.push(params) - }) + const sendEmailInvocations = mailerMock.hijackFunction( + 'sendEmail', + async () => true, + { times: emails.length } + ) const result = await batchCreateServerInvites(apollo, { message, @@ -507,9 +495,9 @@ describe('[Stream & Server Invites]', () => { expect(result.data?.serverInviteBatchCreate).to.be.ok expect(result.errors).to.not.be.ok - expect(emailParamsArr).to.have.length(emails.length) + expect(sendEmailInvocations.length()).to.eq(emails.length) for (const email of emails) { - const emailParams = emailParamsArr.find((p) => p.to === email) + const emailParams = sendEmailInvocations.args.find(([p]) => p.to === email)[0] expect(emailParams).to.be.ok expect(emailParams.html).to.contain(message) @@ -544,22 +532,22 @@ describe('[Stream & Server Invites]', () => { } ] - const emailParamsArr = [] - mailerMock.enable() - mailerMock.mockFunction('sendEmail', (params) => { - emailParamsArr.push(params) - }) + const sendEmailInvocations = mailerMock.hijackFunction( + 'sendEmail', + async () => false, + { times: inputs.length } + ) const result = await batchCreateStreamInvites(apollo, inputs) expect(result.data?.streamInviteBatchCreate).to.be.ok expect(result.errors).to.not.be.ok - expect(emailParamsArr).to.have.length(inputs.length) + expect(sendEmailInvocations.length()).to.eq(inputs.length) for (const inputData of inputs) { - const emailParams = emailParamsArr.find((p) => + const emailParams = sendEmailInvocations.args.find(([p]) => inputData.email ? p.to === inputData.email : p.to === otherGuy.email - ) + )[0] expect(emailParams).to.be.ok expect(emailParams.html).to.contain(inputData.message) expect(emailParams.text).to.contain(inputData.message) diff --git a/packages/server/modules/shared/errors/base.ts b/packages/server/modules/shared/errors/base.ts index ea5730f1f..dc2575671 100644 --- a/packages/server/modules/shared/errors/base.ts +++ b/packages/server/modules/shared/errors/base.ts @@ -18,7 +18,7 @@ export class BaseError extends VError { static defaultMessage = 'Unexpected error occurred!' constructor( - message: string | null | undefined, + message?: string | null | undefined, options: Options | Error | undefined = undefined ) { // Resolve options correctly @@ -52,6 +52,6 @@ export class BaseError extends VError { * Get collected info of this object and previous errors */ info() { - return BaseError.info(this as unknown as Error) + return BaseError.info(this) } } diff --git a/packages/server/modules/shared/errors/index.ts b/packages/server/modules/shared/errors/index.ts index f87a62662..13a8b1d31 100644 --- a/packages/server/modules/shared/errors/index.ts +++ b/packages/server/modules/shared/errors/index.ts @@ -55,4 +55,20 @@ export class ContextError extends BaseError { static defaultMessage = 'The context is missing from the request' } +export class MisconfiguredEnvironmentError extends BaseError { + static code = 'MISCONFIGURED_ENVIRONMENT_ERROR' + static defaultMessage = + 'An error occurred due to the server environment being misconfigured' +} + +export class UninitializedResourceAccessError extends BaseError { + static code = 'UNINITIALIZED_RESOURCE_ACCESS_ERROR' + static defaultMessage = 'Attempted to use uninitialized resources' +} + +export class UnexpectedErrorStructureError extends BaseError { + static code = 'UNEXPECTED_ERROR_STRUCTURE_ERROR' + static defaultMessage = 'An unexpected error type was thrown' +} + export { BaseError } diff --git a/packages/server/modules/shared/helpers/bullHelper.ts b/packages/server/modules/shared/helpers/bullHelper.ts new file mode 100644 index 000000000..c1534eb2f --- /dev/null +++ b/packages/server/modules/shared/helpers/bullHelper.ts @@ -0,0 +1,20 @@ +import Redis from 'ioredis' +import Bull from 'bull' +import { getRedisUrl } from '@/modules/shared/helpers/envHelper' + +export function buildBaseQueueOptions(): Bull.QueueOptions { + return { + createClient: (type) => { + // @see https://github.com/OptimalBits/bull/issues/1873 + const client = new Redis(getRedisUrl(), { + ...(['bclient', 'subscriber'].includes(type) + ? { + enableReadyCheck: false, + maxRetriesPerRequest: null + } + : {}) + }) + return client + } + } +} diff --git a/packages/server/modules/shared/helpers/cryptoHelper.js b/packages/server/modules/shared/helpers/cryptoHelper.js deleted file mode 100644 index a78010f20..000000000 --- a/packages/server/modules/shared/helpers/cryptoHelper.js +++ /dev/null @@ -1,12 +0,0 @@ -const crypto = require('crypto') - -function md5(stringValue) { - return crypto - .createHash('md5') - .update(stringValue || '') - .digest('hex') -} - -module.exports = { - md5 -} diff --git a/packages/server/modules/shared/helpers/cryptoHelper.ts b/packages/server/modules/shared/helpers/cryptoHelper.ts new file mode 100644 index 000000000..90f1c1064 --- /dev/null +++ b/packages/server/modules/shared/helpers/cryptoHelper.ts @@ -0,0 +1,18 @@ +import crypto from 'crypto' + +export function md5(val: string): string { + return crypto + .createHash('md5') + .update(val || '') + .digest('hex') +} + +export function base64Encode(val: string): string { + const bufferObj = Buffer.from(val, 'utf8') + return bufferObj.toString('base64') +} + +export function base64Decode(val: string): string { + const bufferObj = Buffer.from(val, 'base64') + return bufferObj.toString('utf8') +} diff --git a/packages/server/modules/shared/helpers/envHelper.ts b/packages/server/modules/shared/helpers/envHelper.ts index 1c5a0d937..16c5a4e2f 100644 --- a/packages/server/modules/shared/helpers/envHelper.ts +++ b/packages/server/modules/shared/helpers/envHelper.ts @@ -1,3 +1,5 @@ +import { MisconfiguredEnvironmentError } from '@/modules/shared/errors' + export function isTestEnv() { return process.env.NODE_ENV === 'test' } @@ -25,3 +27,31 @@ export function getApolloServerVersion() { export function getFileSizeLimitMB() { return parseInt(process.env.FILE_SIZE_LIMIT_MB || '100') } + +export function getRedisUrl() { + if (!process.env.REDIS_URL) { + throw new MisconfiguredEnvironmentError('REDIS_URL env var not configured') + } + + return process.env.REDIS_URL +} + +/** + * Get app base url / canonical url / origin + */ +export function getBaseUrl() { + if (!process.env.CANONICAL_URL) { + throw new MisconfiguredEnvironmentError('CANONICAL_URL env var not configured') + } + + return process.env.CANONICAL_URL +} + +/** + * Whether notification job consumption & handling should be disabled + */ +export function shouldDisableNotificationsConsumption() { + return ['1', 'true'].includes( + process.env.DISABLE_NOTIFICATIONS_CONSUMPTION || 'false' + ) +} diff --git a/packages/server/modules/shared/helpers/errorHelper.ts b/packages/server/modules/shared/helpers/errorHelper.ts new file mode 100644 index 000000000..a4dfa9dcf --- /dev/null +++ b/packages/server/modules/shared/helpers/errorHelper.ts @@ -0,0 +1,30 @@ +import { BaseError, UnexpectedErrorStructureError } from '@/modules/shared/errors' +import { VError } from 'verror' + +/** + * In JS catch clauses can receive not only Errors, but pretty much any other kind of data type, so + * you can use this helper to ensure that whatever is passed in is a real error + */ +export function ensureError( + e: Error | unknown, + fallbackMessage?: string +): Error | BaseError { + if (e instanceof Error) return e + return new UnexpectedErrorStructureError(fallbackMessage, { + info: { + originalError: e + } + }) +} + +/** + * Resolve cause correctly depending on whether its a VError or basic Error + * object + */ +export function getCause(e: Error) { + if (e instanceof VError) { + return VError.cause(e) + } else { + return e.cause + } +} diff --git a/packages/server/modules/shared/helpers/graphqlHelper.ts b/packages/server/modules/shared/helpers/graphqlHelper.ts new file mode 100644 index 000000000..cda162446 --- /dev/null +++ b/packages/server/modules/shared/helpers/graphqlHelper.ts @@ -0,0 +1,15 @@ +import { base64Decode, base64Encode } from '@/modules/shared/helpers/cryptoHelper' + +/** + * Encode cursor to turn it into an opaque & obfuscated value + */ +export function encodeCursor(value: string): string { + return base64Encode(value) +} + +/** + * Decode obfuscated cursor value + */ +export function decodeCursor(value: string): string { + return base64Decode(value) +} diff --git a/packages/server/modules/shared/helpers/typeHelper.ts b/packages/server/modules/shared/helpers/typeHelper.ts index 7a218b36c..86f9b3567 100644 --- a/packages/server/modules/shared/helpers/typeHelper.ts +++ b/packages/server/modules/shared/helpers/typeHelper.ts @@ -1,10 +1,41 @@ +import { RequestDataLoaders } from '@/modules/core/loaders' +import { AuthContext } from '@/modules/shared/authz' import { Express } from 'express' export type Nullable = T | null export type Optional = T | undefined +export type MaybeNullOrUndefined = T | null | undefined export type MaybeAsync = T | Promise +export type MaybeFalsy = T | null | undefined | false | '' | 0 -export type SpeckleModule = { - init: (app: Express) => MaybeAsync - finalize: (app: Express) => MaybeAsync +export type SpeckleModule = Record> = + { + /** + * Initialize the module + * @param app The Express instance + * @param isInitial Whether this initialization method is being invoked for the first time in this + * process. In tests modules can be initialized multiple times. + */ + init: (app: Express, isInitial: boolean) => MaybeAsync + /** + * Finalize initialization. This is only invoked once all of the other modules' `init()` + * hooks are run. + * @param app The Express instance + * @param isInitial Whether this initialization method is being invoked for the first time in this + * process. In tests modules can be initialized multiple times. + */ + finalize?: (app: Express, isInitial: boolean) => MaybeAsync + + /** + * Cleanup resources before the server shuts down + */ + shutdown?: () => MaybeAsync + } & T + +export type GraphQLContext = AuthContext & { + /** + * Request-scoped GraphQL dataloaders + * @see https://github.com/graphql/dataloader + */ + loaders: RequestDataLoaders } diff --git a/packages/server/modules/shared/index.js b/packages/server/modules/shared/index.js index 88cf6c808..6061ad6a0 100644 --- a/packages/server/modules/shared/index.js +++ b/packages/server/modules/shared/index.js @@ -13,13 +13,16 @@ const StreamPubsubEvents = Object.freeze({ StreamDeleted: 'STREAM_DELETED' }) +/** + * GraphQL Subscription PubSub instance + */ const pubsub = new RedisPubSub({ publisher: new Redis(process.env.REDIS_URL), subscriber: new Redis(process.env.REDIS_URL) }) /** - * @typedef {import('@/modules/shared/authz').AuthContext & {loaders: import('@/modules/core/loaders').RequestDataLoaders}} GraphQLContext + * @typedef {import('@/modules/shared/helpers/typeHelper').GraphQLContext} GraphQLContext */ /** diff --git a/packages/server/modules/shared/services/moduleEventEmitterSetup.ts b/packages/server/modules/shared/services/moduleEventEmitterSetup.ts new file mode 100644 index 000000000..559c7a068 --- /dev/null +++ b/packages/server/modules/shared/services/moduleEventEmitterSetup.ts @@ -0,0 +1,81 @@ +import { MaybeAsync } from '@/modules/shared/helpers/typeHelper' +import { modulesDebug } from '@/modules/shared/utils/logger' +import EventEmitter from 'eventemitter2' + +export type ModuleEventEmitterParams = { + moduleName: string + /** + * If you have multiple emitters in a single module, you can use this identify + * each of them differently + */ + namespace?: string +} + +/** + * Initialize Speckle Module scoped event emitter. These can be used to make code more SOLID - instead of + * modifying some code that does X every time you want to do something extra when X occurs, just emit an event + * there and specify the listening code in a more appropriate module. + * + * Example: Instead of comment mentions being sent out from the comment repository's "createComment" function, + * this repo function emits a COMMENT_CREATED event, that is then handled in a more appropriate module - the speckle + * Notifications module. + */ +export function initializeModuleEventEmitter

>( + params: ModuleEventEmitterParams +) { + const { moduleName, namespace } = params + const identifier = namespace ? `${moduleName}-${namespace}` : moduleName + + const debug = modulesDebug.extend(identifier).extend('events') + + const errHandler = (e: unknown) => { + debug(`Unhandled ${identifier} event emitter error`, e) + } + + const emitter = new EventEmitter() + emitter.on('uncaughtException', errHandler) + emitter.on('error', errHandler) + + return { + /** + * Emit a module event. This function must be awaited to ensure all listeners + * execute. Any errors thrown in the listeners will bubble up and throw from + * the part of code that triggers this emit() call. + */ + emit: async (eventName: K, payload: P[K]) => { + return await emitter.emitAsync(eventName, payload) + }, + + /** + * Listen for module events. Any errors thrown here will bubble out of where + * emit() was invoked. + * + * @returns Callback for stopping listening + */ + listen: ( + eventName: K, + handler: (payload: P[K]) => MaybeAsync + ) => { + emitter.on(eventName, handler, { + async: true, + promisify: true + }) + + return () => { + emitter.removeListener(eventName, handler) + } + }, + + /** + * Destroy event emitter + */ + destroy() { + emitter.removeAllListeners() + }, + + /** + * Debugger scoped to this module event emitter + */ + debug + } +} diff --git a/packages/server/modules/shared/utils/logger.ts b/packages/server/modules/shared/utils/logger.ts new file mode 100644 index 000000000..2f4328b6e --- /dev/null +++ b/packages/server/modules/shared/utils/logger.ts @@ -0,0 +1,7 @@ +import dbg from 'debug' + +const debug = dbg('speckle') + +export const modulesDebug = debug.extend('modules') +export const notificationsDebug = debug.extend('notifications') +export const cliDebug = debug.extend('cli') diff --git a/packages/server/nyc.config.js b/packages/server/nyc.config.js index 50d0ebb5f..e557e603a 100644 --- a/packages/server/nyc.config.js +++ b/packages/server/nyc.config.js @@ -3,6 +3,7 @@ const testFileExtensions = ['ts', 'js'] module.exports = { exclude: [ `**/migrations/*.{${testFileExtensions}}`, + `**/modules/cli/**/*.{${testFileExtensions}}`, '**/*.spec.{js,ts}', // Default exclusions: https://github.com/istanbuljs/schema/blob/master/default-exclude.js diff --git a/packages/server/package.json b/packages/server/package.json index 6e7980a53..4428ecd45 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -19,13 +19,13 @@ "build:watch": "tsc -p ./tsconfig.build.json -w", "run:watch": "cross-env NODE_ENV=development DEBUG='speckle:*' nodemon ./bin/www --watch ./dist --watch ./assets --watch ./bin/www -e js,ts,graphql,env,gql", "dev": "concurrently -r \"npm:build:watch\" \"npm:run:watch\"", - "dev:server:test": "cross-env NODE_ENV=test DEBUG='speckle:*' node ./bin/ts-www", + "dev:server:test": "cross-env DISABLE_NOTIFICATIONS_CONSUMPTION=true NODE_ENV=test DEBUG='speckle:*' node ./bin/ts-www", "test": "cross-env NODE_ENV=test mocha", "test:coverage": "cross-env NODE_ENV=test nyc --reporter lcov mocha", "test:report": "yarn test:coverage -- --reporter mocha-junit-reporter --reporter-options mochaFile=reports/test-results.xml", "lint": "eslint . --ext .js,.ts", "lint:tsc": "tsc --noEmit", - "cli": "ts-node ./modules/cli/index.js", + "cli": "cross-env NODE_ENV=development DEBUG='speckle:*' ts-node ./modules/cli/index.js", "migrate": "yarn cli db migrate", "gqlgen": "graphql-codegen --config codegen.yml" }, @@ -38,15 +38,18 @@ "apollo-server-express": "^2.19.0", "apollo-server-testing": "^2.19.0", "bcrypt": "^5.0.0", + "bull": "^4.8.5", "busboy": "^1.4.0", "compression": "^1.7.4", "connect-redis": "^6.1.1", "cors": "^2.8.5", "crypto-random-string": "^3.2.0", "dataloader": "^2.0.0", + "dayjs": "^1.11.5", "debug": "^4.3.1", "dotenv": "^8.2.0", "ejs": "^3.1.8", + "eventemitter2": "^6.4.7", "express": "^4.17.3", "express-async-errors": "^3.1.1", "express-session": "^1.17.1", @@ -56,7 +59,7 @@ "graphql-subscriptions": "^2.0.0", "graphql-tag": "^2.11.0", "graphql-tools": "^4.0.7", - "ioredis": "^4.19.4", + "ioredis": "^5.2.2", "knex": "^2.0.0", "lodash": "^4.17.21", "module-alias": "^2.2.2", @@ -85,21 +88,30 @@ "zxcvbn": "^4.4.2" }, "devDependencies": { + "@bull-board/express": "^4.2.2", "@faker-js/faker": "^7.1.0", "@graphql-codegen/cli": "2.11.3", "@graphql-codegen/typescript": "2.7.2", "@graphql-codegen/typescript-operations": "^2.5.2", "@graphql-codegen/typescript-resolvers": "2.7.2", "@swc/core": "^1.2.222", + "@tiptap/core": "^2.0.0-beta.176", + "@types/bull": "^3.15.9", "@types/compression": "^1.7.2", "@types/debug": "^4.1.7", + "@types/deep-equal-in-any-order": "^1.0.1", "@types/ejs": "^3.1.1", "@types/express": "^4.17.13", "@types/lodash": "^4.14.180", "@types/mocha": "^7.0.2", + "@types/mock-require": "^2.0.1", "@types/module-alias": "^2.0.1", + "@types/nodemailer": "^6.4.5", + "@types/sanitize-html": "^2.6.2", + "@types/supertest": "^2.0.12", "@types/verror": "^1.10.6", "@types/yargs": "^17.0.10", + "@types/zxcvbn": "^4.4.1", "@typescript-eslint/eslint-plugin": "^5.32.0", "@typescript-eslint/parser": "^5.32.0", "apollo-cache-inmemory": "^1.6.6", @@ -125,6 +137,7 @@ "supertest": "^4.0.2", "ts-node": "^10.9.1", "tsconfig-paths": "^4.0.0", + "type-fest": "^2.19.0", "typescript": "^4.6.4", "ws": "^7.5.7", "yargs": "^17.3.1" diff --git a/packages/server/readme.md b/packages/server/readme.md index 596f555ab..71713cec7 100644 --- a/packages/server/readme.md +++ b/packages/server/readme.md @@ -60,6 +60,14 @@ You can get the best DX by typing your resolvers with the `Resolvers` type and t To create new migrations use `yarn migrate create`. Note that migrations are only ever read from the `./dist` folder to avoid scenarious when both the TS and JS version of the same migration is executed, so if you ever create a new migration make sure you build the app into `/dist` if you want it to be applied. +### CLI + +We've got a yargs based dev-only CLI that you can run and extend with useful commands. Run it through `yarn cli` and add new commands under `./modules/cli` + +### Bull queue monitoring + +Use `yarn cli bull monitor` to serve a Web UI for our Bull queues (e.g. Notifications queues). In the prod env we don't retain old jobs, but locally these older results aren't deleted and you'll see them in this Web UI. + ## Server & Apps ### Frontend diff --git a/packages/server/test/authHelper.js b/packages/server/test/authHelper.js deleted file mode 100644 index 42c3be5c3..000000000 --- a/packages/server/test/authHelper.js +++ /dev/null @@ -1,16 +0,0 @@ -const { AllScopes } = require('@/modules/core/helpers/mainConstants') -const { createPersonalAccessToken } = require('@/modules/core/services/tokens') - -/** - * Create an auth token for the specified user (use only during tests, of course) - * @param {string} userId User's ID - * @param {string[]} scopes Specify scopes you want to allow. Defaults to all scopes. - * @returns {Promise} - */ -async function createAuthTokenForUser(userId, scopes = AllScopes) { - return await createPersonalAccessToken(userId, 'test-runner-token', scopes) -} - -module.exports = { - createAuthTokenForUser -} diff --git a/packages/server/test/authHelper.ts b/packages/server/test/authHelper.ts new file mode 100644 index 000000000..b2768b6a5 --- /dev/null +++ b/packages/server/test/authHelper.ts @@ -0,0 +1,52 @@ +import { AllScopes } from '@/modules/core/helpers/mainConstants' +import { UserRecord } from '@/modules/core/helpers/types' +import { createPersonalAccessToken } from '@/modules/core/services/tokens' +import { createUser } from '@/modules/core/services/users' +import { kebabCase, omit } from 'lodash' + +export type BasicTestUser = { + name: string + email: string + password?: string + /** + * Will be set by createTestUser(), but you need to set a default value to '' + * so that you don't have to check if its empty cause of TS + */ + id: string +} & Partial + +/** + * Create basic user for tests and on success mutate the input object to have + * the new ID + */ +export async function createTestUser(userObj: BasicTestUser) { + if (!userObj.password) { + userObj.password = 'some-random-password-123456789#!@' + } + + if (!userObj.email) { + userObj.email = `${kebabCase(userObj.name)}@someemail.com` + } + + const id = await createUser(omit(userObj, ['id'])) + userObj.id = id +} + +/** + * Create multiple users for tests and update them to include their ID + */ +export async function createTestUsers(userObjs: BasicTestUser[]) { + await Promise.all(userObjs.map((o) => createTestUser(o))) +} + +/** + * Create an auth token for the specified user (use only during tests, of course) + * @param userId User's ID + * @param Specify scopes you want to allow. Defaults to all scopes. + */ +export async function createAuthTokenForUser( + userId: string, + scopes: string[] = AllScopes +): Promise { + return await createPersonalAccessToken(userId, 'test-runner-token', scopes) +} diff --git a/packages/server/test/graphql/accessRequests.ts b/packages/server/test/graphql/accessRequests.ts new file mode 100644 index 000000000..bbf8e301b --- /dev/null +++ b/packages/server/test/graphql/accessRequests.ts @@ -0,0 +1,110 @@ +import { + CreateStreamAccessRequestMutation, + CreateStreamAccessRequestMutationVariables, + GetPendingStreamAccessRequestsQuery, + GetPendingStreamAccessRequestsQueryVariables, + GetStreamAccessRequestQuery, + GetStreamAccessRequestQueryVariables, + UseStreamAccessRequestMutation, + UseStreamAccessRequestMutationVariables +} from '@/test/graphql/generated/graphql' +import { executeOperation } from '@/test/graphqlHelper' +import { ApolloServer, gql } from 'apollo-server-express' + +const basicStreamAccessRequestFragment = gql` + fragment BasicStreamAccessRequestFields on StreamAccessRequest { + id + requester { + id + name + } + requesterId + streamId + createdAt + } +` + +const createStreamAccessRequestMutation = gql` + mutation CreateStreamAccessRequest($streamId: String!) { + streamAccessRequestCreate(streamId: $streamId) { + ...BasicStreamAccessRequestFields + } + } + + ${basicStreamAccessRequestFragment} +` + +const getStreamAccessRequestQuery = gql` + query GetStreamAccessRequest($streamId: String!) { + streamAccessRequest(streamId: $streamId) { + ...BasicStreamAccessRequestFields + } + } + + ${basicStreamAccessRequestFragment} +` + +const getPendingStreamAccessRequestsQuery = gql` + query GetPendingStreamAccessRequests($streamId: String!) { + stream(id: $streamId) { + id + name + pendingAccessRequests { + ...BasicStreamAccessRequestFields + stream { + id + name + } + } + } + } + + ${basicStreamAccessRequestFragment} +` + +const useStreamAccessRequestMutation = gql` + mutation UseStreamAccessRequest( + $requestId: String! + $accept: Boolean! + $role: StreamRole! = STREAM_CONTRIBUTOR + ) { + streamAccessRequestUse(requestId: $requestId, accept: $accept, role: $role) + } +` + +export const createStreamAccessRequest = ( + apollo: ApolloServer, + variables: CreateStreamAccessRequestMutationVariables +) => + executeOperation< + CreateStreamAccessRequestMutation, + CreateStreamAccessRequestMutationVariables + >(apollo, createStreamAccessRequestMutation, variables) + +export const getStreamAccessRequest = ( + apollo: ApolloServer, + variables: GetStreamAccessRequestQueryVariables +) => + executeOperation( + apollo, + getStreamAccessRequestQuery, + variables + ) + +export const getPendingStreamAccessRequests = ( + apollo: ApolloServer, + variables: GetPendingStreamAccessRequestsQueryVariables +) => + executeOperation< + GetPendingStreamAccessRequestsQuery, + GetPendingStreamAccessRequestsQueryVariables + >(apollo, getPendingStreamAccessRequestsQuery, variables) + +export const useStreamAccessRequest = ( + apollo: ApolloServer, + variables: UseStreamAccessRequestMutationVariables +) => + executeOperation< + UseStreamAccessRequestMutation, + UseStreamAccessRequestMutationVariables + >(apollo, useStreamAccessRequestMutation, variables) diff --git a/packages/server/test/graphql/comments.ts b/packages/server/test/graphql/comments.ts new file mode 100644 index 000000000..e698eeb04 --- /dev/null +++ b/packages/server/test/graphql/comments.ts @@ -0,0 +1,112 @@ +import { + CreateCommentMutation, + CreateCommentMutationVariables, + CreateReplyMutation, + CreateReplyMutationVariables, + GetCommentQuery, + GetCommentQueryVariables, + GetCommentsQuery, + GetCommentsQueryVariables +} from '@/test/graphql/generated/graphql' +import { executeOperation } from '@/test/graphqlHelper' +import { ApolloServer, gql } from 'apollo-server-express' + +const commentWithRepliesFragment = gql` + fragment CommentWithReplies on Comment { + id + text { + doc + attachments { + id + fileName + streamId + } + } + replies(limit: 10) { + items { + id + text { + doc + attachments { + id + fileName + streamId + } + } + } + } + } +` + +const createCommentMutation = gql` + mutation CreateComment($input: CommentCreateInput!) { + commentCreate(input: $input) + } +` + +const createReplyMutation = gql` + mutation CreateReply($input: ReplyCreateInput!) { + commentReply(input: $input) + } +` + +const getCommentQuery = gql` + query GetComment($id: String!, $streamId: String!) { + comment(id: $id, streamId: $streamId) { + ...CommentWithReplies + } + } + + ${commentWithRepliesFragment} +` + +const getCommentsQuery = gql` + query GetComments($streamId: String!, $cursor: String) { + comments(streamId: $streamId, limit: 10, cursor: $cursor) { + totalCount + cursor + items { + ...CommentWithReplies + } + } + } + + ${commentWithRepliesFragment} +` + +export const createComment = ( + apollo: ApolloServer, + variables: CreateCommentMutationVariables +) => + executeOperation( + apollo, + createCommentMutation, + variables + ) + +export const createReply = ( + apollo: ApolloServer, + variables: CreateReplyMutationVariables +) => + executeOperation( + apollo, + createReplyMutation, + variables + ) + +export const getComment = (apollo: ApolloServer, variables: GetCommentQueryVariables) => + executeOperation( + apollo, + getCommentQuery, + variables + ) + +export const getComments = ( + apollo: ApolloServer, + variables: GetCommentsQueryVariables +) => + executeOperation( + apollo, + getCommentsQuery, + variables + ) diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index 7cd2dc5c4..92f3c659d 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -11,9 +11,9 @@ export type Scalars = { Int: number; Float: number; BigInt: any; - DateTime: any; + DateTime: string; EmailAddress: any; - JSONObject: any; + JSONObject: Record; }; export type Activity = { @@ -181,6 +181,78 @@ export type BranchUpdateInput = { streamId: Scalars['String']; }; +export type Comment = { + __typename?: 'Comment'; + archived: Scalars['Boolean']; + authorId: Scalars['String']; + createdAt?: Maybe; + data?: Maybe; + id: Scalars['String']; + reactions?: Maybe>>; + /** Gets the replies to this comment. */ + replies?: Maybe; + /** Resources that this comment targets. Can be a mixture of either one stream, or multiple commits and objects. */ + resources: Array>; + screenshot?: Maybe; + text: SmartTextEditorValue; + /** The time this comment was last updated. Corresponds also to the latest reply to this comment, if any. */ + updatedAt?: Maybe; + /** The last time you viewed this comment. Present only if an auth'ed request. Relevant only if a top level commit. */ + viewedAt?: Maybe; +}; + + +export type CommentRepliesArgs = { + cursor?: InputMaybe; + limit?: InputMaybe; +}; + +export type CommentActivityMessage = { + __typename?: 'CommentActivityMessage'; + comment: Comment; + type: Scalars['String']; +}; + +export type CommentCollection = { + __typename?: 'CommentCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']; +}; + +export type CommentCreateInput = { + /** IDs of uploaded blobs that should be attached to this comment */ + blobIds: Array; + data: Scalars['JSONObject']; + /** + * Specifies the resources this comment is linked to. There are several use cases: + * - a comment targets only one resource (commit or object) + * - a comment targets one or more resources (commits or objects) + * - a comment targets only a stream + */ + resources: Array>; + screenshot?: InputMaybe; + streamId: Scalars['String']; + /** ProseMirror document object */ + text?: InputMaybe; +}; + +export type CommentEditInput = { + /** IDs of uploaded blobs that should be attached to this comment */ + blobIds: Array; + id: Scalars['String']; + streamId: Scalars['String']; + /** ProseMirror document object */ + text?: InputMaybe; +}; + +export type CommentThreadActivityMessage = { + __typename?: 'CommentThreadActivityMessage'; + data?: Maybe; + reply?: Maybe; + type: Scalars['String']; +}; + export type Commit = { __typename?: 'Commit'; /** All the recent activity on this commit in chronological order */ @@ -189,6 +261,17 @@ export type Commit = { authorId?: Maybe; authorName?: Maybe; branchName?: Maybe; + /** + * The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this commit's id. + * E.g., + * ``` + * query{ + * comments(streamId:"streamId" resources:[{resourceType: commit, resourceId:"commitId"}] ){ + * ... + * } + * ``` + */ + commentCount: Scalars['Int']; createdAt?: Maybe; id: Scalars['String']; message?: Maybe; @@ -224,6 +307,17 @@ export type CommitCollectionUser = { export type CommitCollectionUserNode = { __typename?: 'CommitCollectionUserNode'; branchName?: Maybe; + /** + * The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this commit's id. + * E.g., + * ``` + * query{ + * comments(streamId:"streamId" resources:[{resourceType: commit, resourceId:"commitId"}] ){ + * ... + * } + * ``` + */ + commentCount: Scalars['Int']; createdAt?: Maybe; id: Scalars['String']; message?: Maybe; @@ -267,6 +361,16 @@ export type CommitUpdateInput = { streamId: Scalars['String']; }; +export enum DiscoverableStreamsSortType { + CreatedDate = 'CREATED_DATE', + FavoritesCount = 'FAVORITES_COUNT' +} + +export type DiscoverableStreamsSortingInput = { + direction: SortDirection; + type: DiscoverableStreamsSortType; +}; + export type FileUpload = { __typename?: 'FileUpload'; branchName?: Maybe; @@ -322,6 +426,16 @@ export type Mutation = { branchCreate: Scalars['String']; branchDelete: Scalars['Boolean']; branchUpdate: Scalars['Boolean']; + /** Archives a comment. */ + commentArchive: Scalars['Boolean']; + /** Creates a comment */ + commentCreate: Scalars['String']; + /** Edits a comment. */ + commentEdit: Scalars['Boolean']; + /** Adds a reply to a comment. */ + commentReply: Scalars['String']; + /** Flags a comment as viewed by you (the logged in user). */ + commentView: Scalars['Boolean']; commitCreate: Scalars['String']; commitDelete: Scalars['Boolean']; commitReceive: Scalars['Boolean']; @@ -331,10 +445,16 @@ export type Mutation = { /** Re-send a pending invite */ inviteResend: Scalars['Boolean']; objectCreate: Array>; + /** (Re-)send the account verification e-mail */ + requestVerification: Scalars['Boolean']; serverInfoUpdate?: Maybe; serverInviteBatchCreate: Scalars['Boolean']; /** Invite a new user to the speckle server and return the invite ID */ serverInviteCreate: Scalars['Boolean']; + /** Request access to a specific stream */ + streamAccessRequestCreate: StreamAccessRequest; + /** Accept or decline a stream access request. Must be a stream owner to invoke this. */ + streamAccessRequestUse: Scalars['Boolean']; /** Creates a new stream. */ streamCreate?: Maybe; /** Deletes an existing stream. */ @@ -356,11 +476,15 @@ export type Mutation = { /** Update permissions of a user on a given stream. */ streamUpdatePermission?: Maybe; streamsDelete: Scalars['Boolean']; + /** Used for broadcasting real time typing status in comment threads. Does not persist any info. */ + userCommentThreadActivityBroadcast: Scalars['Boolean']; /** Delete a user's account. */ userDelete: Scalars['Boolean']; userRoleChange: Scalars['Boolean']; /** Edits a user's profile. */ userUpdate: Scalars['Boolean']; + /** Used for broadcasting real time chat head bubbles and status. Does not persist any info. */ + userViewerActivityBroadcast: Scalars['Boolean']; /** Creates a new webhook on a stream */ webhookCreate: Scalars['String']; /** Deletes an existing webhook */ @@ -420,6 +544,34 @@ export type MutationBranchUpdateArgs = { }; +export type MutationCommentArchiveArgs = { + archived?: Scalars['Boolean']; + commentId: Scalars['String']; + streamId: Scalars['String']; +}; + + +export type MutationCommentCreateArgs = { + input: CommentCreateInput; +}; + + +export type MutationCommentEditArgs = { + input: CommentEditInput; +}; + + +export type MutationCommentReplyArgs = { + input: ReplyCreateInput; +}; + + +export type MutationCommentViewArgs = { + commentId: Scalars['String']; + streamId: Scalars['String']; +}; + + export type MutationCommitCreateArgs = { commit: CommitCreateInput; }; @@ -470,6 +622,18 @@ export type MutationServerInviteCreateArgs = { }; +export type MutationStreamAccessRequestCreateArgs = { + streamId: Scalars['String']; +}; + + +export type MutationStreamAccessRequestUseArgs = { + accept: Scalars['Boolean']; + requestId: Scalars['String']; + role?: StreamRole; +}; + + export type MutationStreamCreateArgs = { stream: StreamCreateInput; }; @@ -534,6 +698,13 @@ export type MutationStreamsDeleteArgs = { }; +export type MutationUserCommentThreadActivityBroadcastArgs = { + commentId: Scalars['String']; + data?: InputMaybe; + streamId: Scalars['String']; +}; + + export type MutationUserDeleteArgs = { userConfirmation: UserDeleteInput; }; @@ -549,6 +720,13 @@ export type MutationUserUpdateArgs = { }; +export type MutationUserViewerActivityBroadcastArgs = { + data?: InputMaybe; + resourceId: Scalars['String']; + streamId: Scalars['String']; +}; + + export type MutationWebhookCreateArgs = { webhook: WebhookCreateInput; }; @@ -571,6 +749,17 @@ export type Object = { * **NOTE**: Providing any of the two last arguments ( `query`, `orderBy` ) will trigger a different code branch that executes a much more expensive SQL query. It is not recommended to do so for basic clients that are interested in purely getting all the objects of a given commit. */ children: ObjectCollection; + /** + * The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this object's id. + * E.g., + * ``` + * query{ + * comments(streamId:"streamId" resources:[{resourceType: object, resourceId:"objectId"}] ){ + * ... + * } + * ``` + */ + commentCount: Scalars['Int']; createdAt?: Maybe; /** The full object, with all its props & other things. **NOTE:** If you're requesting objects for the purpose of recreating & displaying, you probably only want to request this specific field. */ data?: Maybe; @@ -603,6 +792,27 @@ export type ObjectCreateInput = { streamId: Scalars['String']; }; +export type PasswordStrengthCheckFeedback = { + __typename?: 'PasswordStrengthCheckFeedback'; + suggestions: Array; + warning?: Maybe; +}; + +export type PasswordStrengthCheckResults = { + __typename?: 'PasswordStrengthCheckResults'; + /** Verbal feedback to help choose better passwords. set when score <= 2. */ + feedback: PasswordStrengthCheckFeedback; + /** + * Integer from 0-4 (useful for implementing a strength bar): + * 0 too guessable: risky password. (guesses < 10^3) + * 1 very guessable: protection from throttled online attacks. (guesses < 10^6) + * 2 somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8) + * 3 safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10) + * 4 very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10) + */ + score: Scalars['Int']; +}; + export type PendingStreamCollaborator = { __typename?: 'PendingStreamCollaborator'; id: Scalars['String']; @@ -623,6 +833,7 @@ export type Query = { __typename?: 'Query'; /** Stare into the void. */ _?: Maybe; + /** All the streams of the server. Available to admins only. */ adminStreams?: Maybe; /** * Get all (or search for specific) users, registered or invited, from the server in a paginated view. @@ -633,12 +844,24 @@ export type Query = { app?: Maybe; /** Returns all the publicly available apps on this server. */ apps?: Maybe>>; + comment?: Maybe; + /** + * This query can be used in the following ways: + * - get all the comments for a stream: **do not pass in any resource identifiers**. + * - get the comments targeting any of a set of provided resources (comments/objects): **pass in an array of resources.** + */ + comments?: Maybe; + /** All of the discoverable streams of the server */ + discoverableStreams?: Maybe; serverInfo: ServerInfo; + serverStats: ServerStats; /** * Returns a specific stream. Will throw an authorization error if active user isn't authorized * to see it. */ stream?: Maybe; + /** Get authed user's stream access request */ + streamAccessRequest?: Maybe; /** * Look for an invitation to a stream, for the current user (authed or not). If token * isn't specified, the server will look for any valid invite. @@ -648,12 +871,10 @@ export type Query = { streamInvites: Array; /** All the streams of the current user, pass in the `query` parameter to search by name, description or ID. */ streams?: Maybe; - /** - * Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header). - * If ID is provided, admin access is required - */ + /** Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header). */ user?: Maybe; - userPwdStrength?: Maybe; + /** Validate password strength */ + userPwdStrength: PasswordStrengthCheckResults; /** * Search for users and return limited metadata about them, if you have the server:user role. * The query looks for matches in name & email @@ -683,11 +904,38 @@ export type QueryAppArgs = { }; +export type QueryCommentArgs = { + id: Scalars['String']; + streamId: Scalars['String']; +}; + + +export type QueryCommentsArgs = { + archived?: Scalars['Boolean']; + cursor?: InputMaybe; + limit?: InputMaybe; + resources?: InputMaybe>>; + streamId: Scalars['String']; +}; + + +export type QueryDiscoverableStreamsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; + sort?: InputMaybe; +}; + + export type QueryStreamArgs = { id: Scalars['String']; }; +export type QueryStreamAccessRequestArgs = { + streamId: Scalars['String']; +}; + + export type QueryStreamInviteArgs = { streamId: Scalars['String']; token?: InputMaybe; @@ -718,6 +966,34 @@ export type QueryUserSearchArgs = { query: Scalars['String']; }; +export type ReplyCreateInput = { + /** IDs of uploaded blobs that should be attached to this reply */ + blobIds: Array; + data?: InputMaybe; + parentComment: Scalars['String']; + streamId: Scalars['String']; + /** ProseMirror document object */ + text?: InputMaybe; +}; + +export type ResourceIdentifier = { + __typename?: 'ResourceIdentifier'; + resourceId: Scalars['String']; + resourceType: ResourceType; +}; + +export type ResourceIdentifierInput = { + resourceId: Scalars['String']; + resourceType: ResourceType; +}; + +export enum ResourceType { + Comment = 'comment', + Commit = 'commit', + Object = 'object', + Stream = 'stream' +} + /** Available roles. */ export type Role = { __typename?: 'Role'; @@ -800,6 +1076,22 @@ export type ServerInviteCreateInput = { message?: InputMaybe; }; +export type ServerStats = { + __typename?: 'ServerStats'; + /** An array of objects currently structured as { created_month: Date, count: int }. */ + commitHistory?: Maybe>>; + /** An array of objects currently structured as { created_month: Date, count: int }. */ + objectHistory?: Maybe>>; + /** An array of objects currently structured as { created_month: Date, count: int }. */ + streamHistory?: Maybe>>; + totalCommitCount: Scalars['Int']; + totalObjectCount: Scalars['Int']; + totalStreamCount: Scalars['Int']; + totalUserCount: Scalars['Int']; + /** An array of objects currently structured as { created_month: Date, count: int }. */ + userHistory?: Maybe>>; +}; + export type SmartTextEditorValue = { __typename?: 'SmartTextEditorValue'; /** File attachments, if any */ @@ -815,6 +1107,11 @@ export type SmartTextEditorValue = { version: Scalars['String']; }; +export enum SortDirection { + Asc = 'ASC', + Desc = 'DESC' +} + export type Stream = { __typename?: 'Stream'; /** All the recent activity on this stream in chronological order */ @@ -826,6 +1123,17 @@ export type Stream = { branch?: Maybe; branches?: Maybe; collaborators: Array; + /** + * The total number of comments for this stream. To actually get the comments, use the comments query without passing in a resource array. E.g.: + * + * ``` + * query{ + * comments(streamId:"streamId"){ + * ... + * } + * ``` + */ + commentCount: Scalars['Int']; commit?: Maybe; commits?: Maybe; createdAt: Scalars['DateTime']; @@ -838,9 +1146,17 @@ export type Stream = { /** Returns a list of all the file uploads for this stream. */ fileUploads?: Maybe>>; id: Scalars['String']; + /** + * Whether the stream (if public) can be found on public stream exploration pages + * and searches + */ + isDiscoverable: Scalars['Boolean']; + /** Whether the stream can be viewed by non-contributors */ isPublic: Scalars['Boolean']; name: Scalars['String']; object?: Maybe; + /** Pending stream access requests */ + pendingAccessRequests?: Maybe>; /** Collaborators who have been invited, but not yet accepted. */ pendingCollaborators?: Maybe>; /** Your role for this stream. `null` if request is not authenticated, or the stream is not explicitly shared with you. */ @@ -908,6 +1224,18 @@ export type StreamWebhooksArgs = { id?: InputMaybe; }; +/** Created when a user requests to become a contributor on a stream */ +export type StreamAccessRequest = { + __typename?: 'StreamAccessRequest'; + createdAt: Scalars['DateTime']; + id: Scalars['ID']; + requester: LimitedUser; + requesterId: Scalars['String']; + /** Can only be selected if authed user has proper access */ + stream: Stream; + streamId: Scalars['String']; +}; + export type StreamCollaborator = { __typename?: 'StreamCollaborator'; avatar?: Maybe; @@ -926,6 +1254,12 @@ export type StreamCollection = { export type StreamCreateInput = { description?: InputMaybe; + /** + * Whether the stream (if public) can be found on public stream exploration pages + * and searches + */ + isDiscoverable?: InputMaybe; + /** Whether the stream can be viewed by non-contributors */ isPublic?: InputMaybe; name?: InputMaybe; /** Optionally specify user IDs of users that you want to invite to be contributors to this stream */ @@ -956,6 +1290,12 @@ export type StreamUpdateInput = { allowPublicComments?: InputMaybe; description?: InputMaybe; id: Scalars['String']; + /** + * Whether the stream (if public) can be found on public stream exploration pages + * and searches + */ + isDiscoverable?: InputMaybe; + /** Whether the stream can be viewed by non-contributors */ isPublic?: InputMaybe; name?: InputMaybe; }; @@ -976,6 +1316,18 @@ export type Subscription = { branchDeleted?: Maybe; /** Subscribe to branch updated event. */ branchUpdated?: Maybe; + /** + * Subscribe to new comment events. There's two ways to use this subscription: + * - for a whole stream: do not pass in any resourceIds; this sub will get called whenever a comment (not reply) is added to any of the stream's resources. + * - for a specific resource/set of resources: pass in a list of resourceIds (commit or object ids); this sub will get called when *any* of the resources provided get a comment. + */ + commentActivity: CommentActivityMessage; + /** + * Subscribes to events on a specific comment. Use to find out when: + * - a top level comment is deleted (trigger a deletion event outside) + * - a top level comment receives a reply. + */ + commentThreadActivity: CommentThreadActivityMessage; /** Subscribe to commit created event */ commitCreated?: Maybe; /** Subscribe to commit deleted event */ @@ -996,6 +1348,8 @@ export type Subscription = { * **NOTE**: If someone revokes your permissions on a stream, this subscription will be triggered with an extra value of `revokedBy` in the payload. */ userStreamRemoved?: Maybe; + /** Broadcasts "real-time" location data for viewer users. */ + userViewerActivity?: Maybe; }; @@ -1015,6 +1369,18 @@ export type SubscriptionBranchUpdatedArgs = { }; +export type SubscriptionCommentActivityArgs = { + resourceIds?: InputMaybe>>; + streamId: Scalars['String']; +}; + + +export type SubscriptionCommentThreadActivityArgs = { + commentId: Scalars['String']; + streamId: Scalars['String']; +}; + + export type SubscriptionCommitCreatedArgs = { streamId: Scalars['String']; }; @@ -1040,6 +1406,12 @@ export type SubscriptionStreamUpdatedArgs = { streamId?: InputMaybe; }; + +export type SubscriptionUserViewerActivityArgs = { + resourceId: Scalars['String']; + streamId: Scalars['String']; +}; + /** * Full user type, should only be used in the context of admin operations or * when a user is reading/writing info about himself @@ -1065,13 +1437,14 @@ export type User = { email?: Maybe; /** All the streams that a user has favorited */ favoriteStreams?: Maybe; + /** Whether the user has a pending/active email verification token */ + hasPendingVerification?: Maybe; id: Scalars['String']; name?: Maybe; profiles?: Maybe; role?: Maybe; /** All the streams that a user has access to. */ streams?: Maybe; - suuid?: Maybe; timeline?: Maybe; /** Total amount of favorites attached to streams owned by the user */ totalOwnedStreamsFavorites: Scalars['Int']; @@ -1218,6 +1591,70 @@ export type WebhookUpdateInput = { url?: InputMaybe; }; +export type BasicStreamAccessRequestFieldsFragment = { __typename?: 'StreamAccessRequest', id: string, requesterId: string, streamId: string, createdAt: string, requester: { __typename?: 'LimitedUser', id: string, name?: string | null } }; + +export type CreateStreamAccessRequestMutationVariables = Exact<{ + streamId: Scalars['String']; +}>; + + +export type CreateStreamAccessRequestMutation = { __typename?: 'Mutation', streamAccessRequestCreate: { __typename?: 'StreamAccessRequest', id: string, requesterId: string, streamId: string, createdAt: string, requester: { __typename?: 'LimitedUser', id: string, name?: string | null } } }; + +export type GetStreamAccessRequestQueryVariables = Exact<{ + streamId: Scalars['String']; +}>; + + +export type GetStreamAccessRequestQuery = { __typename?: 'Query', streamAccessRequest?: { __typename?: 'StreamAccessRequest', id: string, requesterId: string, streamId: string, createdAt: string, requester: { __typename?: 'LimitedUser', id: string, name?: string | null } } | null }; + +export type GetPendingStreamAccessRequestsQueryVariables = Exact<{ + streamId: Scalars['String']; +}>; + + +export type GetPendingStreamAccessRequestsQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, pendingAccessRequests?: Array<{ __typename?: 'StreamAccessRequest', id: string, requesterId: string, streamId: string, createdAt: string, stream: { __typename?: 'Stream', id: string, name: string }, requester: { __typename?: 'LimitedUser', id: string, name?: string | null } }> | null } | null }; + +export type UseStreamAccessRequestMutationVariables = Exact<{ + requestId: Scalars['String']; + accept: Scalars['Boolean']; + role?: StreamRole; +}>; + + +export type UseStreamAccessRequestMutation = { __typename?: 'Mutation', streamAccessRequestUse: boolean }; + +export type CommentWithRepliesFragment = { __typename?: 'Comment', id: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null, attachments?: Array<{ __typename?: 'BlobMetadata', id: string, fileName: string, streamId: string }> | null }, replies?: { __typename?: 'CommentCollection', items: Array<{ __typename?: 'Comment', id: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null, attachments?: Array<{ __typename?: 'BlobMetadata', id: string, fileName: string, streamId: string }> | null } }> } | null }; + +export type CreateCommentMutationVariables = Exact<{ + input: CommentCreateInput; +}>; + + +export type CreateCommentMutation = { __typename?: 'Mutation', commentCreate: string }; + +export type CreateReplyMutationVariables = Exact<{ + input: ReplyCreateInput; +}>; + + +export type CreateReplyMutation = { __typename?: 'Mutation', commentReply: string }; + +export type GetCommentQueryVariables = Exact<{ + id: Scalars['String']; + streamId: Scalars['String']; +}>; + + +export type GetCommentQuery = { __typename?: 'Query', comment?: { __typename?: 'Comment', id: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null, attachments?: Array<{ __typename?: 'BlobMetadata', id: string, fileName: string, streamId: string }> | null }, replies?: { __typename?: 'CommentCollection', items: Array<{ __typename?: 'Comment', id: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null, attachments?: Array<{ __typename?: 'BlobMetadata', id: string, fileName: string, streamId: string }> | null } }> } | null } | null }; + +export type GetCommentsQueryVariables = Exact<{ + streamId: Scalars['String']; + cursor?: InputMaybe; +}>; + + +export type GetCommentsQuery = { __typename?: 'Query', comments?: { __typename?: 'CommentCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'Comment', id: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null, attachments?: Array<{ __typename?: 'BlobMetadata', id: string, fileName: string, streamId: string }> | null }, replies?: { __typename?: 'CommentCollection', items: Array<{ __typename?: 'Comment', id: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null, attachments?: Array<{ __typename?: 'BlobMetadata', id: string, fileName: string, streamId: string }> | null } }> } | null }> } | null }; + export type CreateServerInviteMutationVariables = Exact<{ input: ServerInviteCreateInput; }>; @@ -1299,6 +1736,8 @@ export type GetStreamPendingCollaboratorsQueryVariables = Exact<{ export type GetStreamPendingCollaboratorsQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, pendingCollaborators?: Array<{ __typename?: 'PendingStreamCollaborator', inviteId: string, title: string, token?: string | null, user?: { __typename?: 'LimitedUser', id: string, name?: string | null } | null }> | null } | null }; +export type BasicStreamFieldsFragment = { __typename?: 'Stream', id: string, name: string, description?: string | null, isPublic: boolean, isDiscoverable: boolean, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string }; + export type LeaveStreamMutationVariables = Exact<{ streamId: Scalars['String']; }>; @@ -1306,6 +1745,36 @@ export type LeaveStreamMutationVariables = Exact<{ export type LeaveStreamMutation = { __typename?: 'Mutation', streamLeave: boolean }; +export type CreateStreamMutationVariables = Exact<{ + stream: StreamCreateInput; +}>; + + +export type CreateStreamMutation = { __typename?: 'Mutation', streamCreate?: string | null }; + +export type UpdateStreamMutationVariables = Exact<{ + stream: StreamUpdateInput; +}>; + + +export type UpdateStreamMutation = { __typename?: 'Mutation', streamUpdate: boolean }; + +export type ReadStreamQueryVariables = Exact<{ + id: Scalars['String']; +}>; + + +export type ReadStreamQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, description?: string | null, isPublic: boolean, isDiscoverable: boolean, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string } | null }; + +export type ReadDiscoverableStreamsQueryVariables = Exact<{ + limit?: Scalars['Int']; + cursor?: InputMaybe; + sort?: InputMaybe; +}>; + + +export type ReadDiscoverableStreamsQuery = { __typename?: 'Query', discoverableStreams?: { __typename?: 'StreamCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Stream', favoritesCount: number, id: string, name: string, description?: string | null, isPublic: boolean, isDiscoverable: boolean, allowPublicComments: boolean, role?: string | null, createdAt: string, updatedAt: string }> | null } | null }; + export type GetAdminUsersQueryVariables = Exact<{ limit?: Scalars['Int']; offset?: Scalars['Int']; @@ -1314,3 +1783,15 @@ export type GetAdminUsersQueryVariables = Exact<{ export type GetAdminUsersQuery = { __typename?: 'Query', adminUsers?: { __typename?: 'AdminUsersListCollection', totalCount: number, items: Array<{ __typename?: 'AdminUsersListItem', id: string, registeredUser?: { __typename?: 'User', id: string, email?: string | null, name?: string | null } | null, invitedUser?: { __typename?: 'ServerInvite', id: string, email: string, invitedBy: { __typename?: 'LimitedUser', id: string, name?: string | null } } | null }> } | null }; + +export type GetPendingEmailVerificationStatusQueryVariables = Exact<{ + id?: InputMaybe; +}>; + + +export type GetPendingEmailVerificationStatusQuery = { __typename?: 'Query', user?: { __typename?: 'User', hasPendingVerification?: boolean | null } | null }; + +export type RequestVerificationMutationVariables = Exact<{ [key: string]: never; }>; + + +export type RequestVerificationMutation = { __typename?: 'Mutation', requestVerification: boolean }; diff --git a/packages/server/test/graphql/streams.js b/packages/server/test/graphql/streams.js deleted file mode 100644 index 407fc2d8d..000000000 --- a/packages/server/test/graphql/streams.js +++ /dev/null @@ -1,22 +0,0 @@ -const { gql } = require('apollo-server-express') - -const leaveStreamMutation = gql` - mutation LeaveStream($streamId: String!) { - streamLeave(streamId: $streamId) - } -` - -module.exports = { - /** - * streamLeave mutation - * @param {import('apollo-server-express').ApolloServer} apollo - */ - leaveStream(apollo, { streamId }) { - return apollo.executeOperation({ - query: leaveStreamMutation, - variables: { - streamId - } - }) - } -} diff --git a/packages/server/test/graphql/streams.ts b/packages/server/test/graphql/streams.ts new file mode 100644 index 000000000..5a44ede0e --- /dev/null +++ b/packages/server/test/graphql/streams.ts @@ -0,0 +1,122 @@ +import { + LeaveStreamMutation, + LeaveStreamMutationVariables, + CreateStreamMutation, + CreateStreamMutationVariables, + UpdateStreamMutationVariables, + UpdateStreamMutation, + ReadStreamQueryVariables, + ReadStreamQuery, + ReadDiscoverableStreamsQueryVariables, + ReadDiscoverableStreamsQuery +} from '@/test/graphql/generated/graphql' +import { executeOperation } from '@/test/graphqlHelper' +import { ApolloServer, gql } from 'apollo-server-express' + +export const basicStreamFieldsFragment = gql` + fragment BasicStreamFields on Stream { + id + name + description + isPublic + isDiscoverable + allowPublicComments + role + createdAt + updatedAt + } +` + +const leaveStreamMutation = gql` + mutation LeaveStream($streamId: String!) { + streamLeave(streamId: $streamId) + } +` + +const createStreamMutation = gql` + mutation CreateStream($stream: StreamCreateInput!) { + streamCreate(stream: $stream) + } +` + +const updateStreamMutation = gql` + mutation UpdateStream($stream: StreamUpdateInput!) { + streamUpdate(stream: $stream) + } +` + +const readStreamQuery = gql` + query ReadStream($id: String!) { + stream(id: $id) { + ...BasicStreamFields + } + } + + ${basicStreamFieldsFragment} +` + +const readDiscoverableStreamsQuery = gql` + query ReadDiscoverableStreams( + $limit: Int! = 25 + $cursor: String + $sort: DiscoverableStreamsSortingInput + ) { + discoverableStreams(limit: $limit, cursor: $cursor, sort: $sort) { + totalCount + cursor + items { + favoritesCount + ...BasicStreamFields + } + } + } + + ${basicStreamFieldsFragment} +` + +export const leaveStream = ( + apollo: ApolloServer, + variables: LeaveStreamMutationVariables +) => + executeOperation( + apollo, + leaveStreamMutation, + variables + ) + +export const createStream = ( + apollo: ApolloServer, + variables: CreateStreamMutationVariables +) => + executeOperation( + apollo, + createStreamMutation, + variables + ) + +export const updateStream = ( + apollo: ApolloServer, + variables: UpdateStreamMutationVariables +) => + executeOperation( + apollo, + updateStreamMutation, + variables + ) + +export const readStream = (apollo: ApolloServer, variables: ReadStreamQueryVariables) => + executeOperation( + apollo, + readStreamQuery, + variables + ) + +export const readDiscoverableStreams = ( + apollo: ApolloServer, + variables: ReadDiscoverableStreamsQueryVariables +) => + executeOperation( + apollo, + readDiscoverableStreamsQuery, + variables + ) diff --git a/packages/server/test/graphql/users.ts b/packages/server/test/graphql/users.ts index bb4fca32c..189777328 100644 --- a/packages/server/test/graphql/users.ts +++ b/packages/server/test/graphql/users.ts @@ -1,7 +1,11 @@ import { ApolloServer, gql } from 'apollo-server-express' import { GetAdminUsersQuery, - GetAdminUsersQueryVariables + GetAdminUsersQueryVariables, + GetPendingEmailVerificationStatusQuery, + GetPendingEmailVerificationStatusQueryVariables, + RequestVerificationMutation, + RequestVerificationMutationVariables } from '@/test/graphql/generated/graphql' import { executeOperation } from '@/test/graphqlHelper' @@ -29,9 +33,20 @@ const adminUsersQuery = gql` } ` -/** - * adminUsers query - */ +const getPendingEmailVerificationStatusQuery = gql` + query GetPendingEmailVerificationStatus($id: String) { + user(id: $id) { + hasPendingVerification + } + } +` + +const requestVerificationMutation = gql` + mutation RequestVerification { + requestVerification + } +` + export async function getAdminUsersList( apollo: ApolloServer, variables: GetAdminUsersQueryVariables @@ -42,3 +57,22 @@ export async function getAdminUsersList( variables ) } + +export const getPendingEmailVerificationStatus = ( + apollo: ApolloServer, + variables: GetPendingEmailVerificationStatusQueryVariables +) => + executeOperation< + GetPendingEmailVerificationStatusQuery, + GetPendingEmailVerificationStatusQueryVariables + >(apollo, getPendingEmailVerificationStatusQuery, variables) + +export const requestVerification = ( + apollo: ApolloServer, + variables?: RequestVerificationMutationVariables +) => + executeOperation( + apollo, + requestVerificationMutation, + variables + ) diff --git a/packages/server/test/hooks.js b/packages/server/test/hooks.js index 55495bbdd..8b1ccd4c8 100644 --- a/packages/server/test/hooks.js +++ b/packages/server/test/hooks.js @@ -1,13 +1,18 @@ -/* istanbul ignore file */ require('../bootstrap') const chai = require('chai') const chaiHttp = require('chai-http') const deepEqualInAnyOrder = require('deep-equal-in-any-order') const knex = require(`@/db/knex`) -const { init, startHttp } = require(`@/app`) +const { init, startHttp, shutdown } = require(`@/app`) +const { default: graphqlChaiPlugin } = require('@/test/plugins/graphql') +// Register chai plugins chai.use(chaiHttp) chai.use(deepEqualInAnyOrder) +chai.use(graphqlChaiPlugin) + +// Register global mocks +require('@/test/mocks/global') const unlock = async () => { const exists = await knex.schema.hasTable('knex_migrations_lock') @@ -71,13 +76,18 @@ exports.mochaHooks = { afterAll: async () => { console.log('running after all') await unlock() + await shutdown() } } -exports.beforeEachContext = async () => { - await exports.truncateTables() +exports.buildApp = async () => { const { app, graphqlServer } = await init() return { app, graphqlServer } } +exports.beforeEachContext = async () => { + await exports.truncateTables() + return await exports.buildApp() +} + exports.initializeTestServer = initializeTestServer diff --git a/packages/server/test/mockHelper.js b/packages/server/test/mockHelper.js deleted file mode 100644 index fc7b8b8ec..000000000 --- a/packages/server/test/mockHelper.js +++ /dev/null @@ -1,103 +0,0 @@ -const { isArray, isFunction } = require('lodash') -const mock = require('mock-require') - -/** - * Mock a module's exported functions with the possibility to conditionally disable & change the mock - * @param {string|string[]} modulePaths Absolute & relative paths to the module being mocked or if you sometimes require - * with the '/index' suffix and sometimes don't you need to specify both options. Multiple options are required - * because of a limitation of mock-require - it doesn't understand that all of these point to the same thing. - * @param {string|string[]} dependencyPaths Paths to modules that use the mocked module and that you - * want to re-require so that if they are already loaded in memory, they're re-required with the new mock. Basically, - * if you've mocked a module, but it's not being used, debug the test and see if the mocked module is maybe required - * by another module that you haven't specified in this list. - */ -function mockRequireModule(modulePaths, dependencyPaths = []) { - modulePaths = isArray(modulePaths) ? modulePaths : [modulePaths] - dependencyPaths = isArray(dependencyPaths) ? dependencyPaths : [dependencyPaths] - - let disabled = false - let functionReplacements = {} - - const originalModule = require(modulePaths[0]) - const mockDefinition = new Proxy(originalModule, { - get(target, prop) { - if (!isFunction(target[prop])) return target[prop] - return function (...args) { - if (disabled || !functionReplacements[prop]) { - return target[prop].apply(this, args) - } - - return functionReplacements[prop].apply(this, args) - } - } - }) - - // Initialize mock with all paths (relative path, absolute alias path - both need to be specified - // cause of a limitation in mock-require) - for (const modulePath of modulePaths) { - mock(modulePath, mockDefinition) - } - - /** - * Re-requires the specified modules, in case they were required before the mock was set up - * and thus don't have the mocked module - */ - const reRequireDependencies = () => { - for (const dependencyPath of dependencyPaths) { - mock.reRequire(dependencyPath) - } - } - reRequireDependencies() - - return { - /** - * Set (or unset) a mocked implementation of a function - * @param {string} functionName - * @param {Function | null | undefined} implementation - */ - mockFunction(functionName, implementation) { - if (implementation) { - functionReplacements[functionName] = implementation - } else { - delete functionReplacements[functionName] - } - }, - /** - * Remove all mocked function implementations - */ - resetMockedFunctions() { - functionReplacements = {} - }, - /** - * Temporarily disable the mock, sending all function calls to the real implementations - */ - disable() { - disabled = true - }, - /** - * Re-enable the mock, if it's been disabled before - */ - enable() { - disabled = false - }, - /** - * Unmock entirely - * Note: All requires done before this point will still point to the mocks - */ - destroy(reRequireDeps = true) { - for (const modulePath of modulePaths) { - mock.stop(modulePath) - } - - if (reRequireDeps) reRequireDependencies() - }, - /** - * Re-require specified dependencies - */ - reRequireDependencies - } -} - -module.exports = { - mockRequireModule -} diff --git a/packages/server/test/mockHelper.ts b/packages/server/test/mockHelper.ts new file mode 100644 index 000000000..e24ce564e --- /dev/null +++ b/packages/server/test/mockHelper.ts @@ -0,0 +1,222 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-var-requires */ +import { MaybeAsync } from '@/modules/shared/helpers/typeHelper' +import { isArray, isFunction } from 'lodash' +import mock from 'mock-require' +import { ConditionalPick } from 'type-fest' + +export type MockedFunctionImplementation = (...args: any[]) => MaybeAsync + +/** + * Mock a module's exported functions with the possibility to conditionally disable & change the mock + * @param {string|string[]} modulePaths Absolute & relative paths to the module being mocked or if you sometimes require + * with the '/index' suffix and sometimes don't you need to specify both options. Multiple options are required + * because of a limitation of mock-require - it doesn't understand that all of these point to the same thing. + * @param {string|string[]} dependencyPaths Paths to modules that use the mocked module and that you + * want to re-require so that if they are already loaded in memory, they're re-required with the new mock. Basically, + * if you've mocked a module, but it's not being used, debug the test and see if the mocked module is maybe required + * by another module that you haven't specified in this list. + */ +export function mockRequireModule< + MockType extends Record = Record +>( + modulePaths: string | string[], + dependencyPaths: string | string[] = [], + params: { preventDestroy?: boolean } = {} +) { + type MockTypeFunctionsOnly = ConditionalPick + type MockTypeFunctionProp = keyof MockTypeFunctionsOnly + + type MockedFunc = ( + ...args: Parameters + ) => ReturnType + + const { preventDestroy } = params + modulePaths = isArray(modulePaths) ? modulePaths : [modulePaths] + dependencyPaths = isArray(dependencyPaths) ? dependencyPaths : [dependencyPaths] + + let isDisabled = false + let functionReplacements: Partial< + Record> + > = {} + + const originalModule = require(modulePaths[0]) as MockType + const mockDefinition = new Proxy(originalModule, { + get(target, prop) { + const realProp = prop as keyof MockTypeFunctionsOnly + const propVal = target[realProp] + + if (!isFunction(propVal)) return propVal + return function (this: unknown, ...args: Parameters[]) { + const potentialReplacement = functionReplacements[realProp] as typeof propVal + if (isDisabled || !potentialReplacement || !isFunction(potentialReplacement)) { + return propVal.apply(this, args) + } + + return potentialReplacement.apply(this, args) + } + } + }) + + // Initialize mock with all paths (relative path, absolute alias path - both need to be specified + // cause of a limitation in mock-require) + for (const modulePath of modulePaths) { + mock(modulePath, mockDefinition) + } + + /** + * Re-requires the specified modules, in case they were required before the mock was set up + * and thus don't have the mocked module + */ + const reRequireDependencies = () => { + for (const dependencyPath of dependencyPaths) { + mock.reRequire(dependencyPath) + } + } + reRequireDependencies() + + const core = { + /** + * Set (or unset) a mocked implementation of a function + */ + mockFunction( + functionName: F, + implementation: MockedFunc + ) { + if (implementation) { + functionReplacements[functionName] = implementation + } else { + delete functionReplacements[functionName] + } + }, + /** + * Remove all mocked function implementations + */ + resetMockedFunctions() { + functionReplacements = {} + }, + /** + * Remove a single function mock + */ + resetMockedFunction(functionName: MockTypeFunctionProp) { + delete functionReplacements[functionName] + }, + /** + * Temporarily disable the mock, sending all function calls to the real implementations + */ + disable() { + isDisabled = true + }, + /** + * Re-enable the mock, if it's been disabled before + */ + enable() { + isDisabled = false + }, + /** + * Unmock entirely + * Note: All requires done before this point will still point to the mocks + */ + destroy(reRequireDeps = true) { + if (preventDestroy) { + isDisabled = true + return + } + + for (const modulePath of modulePaths) { + mock.stop(modulePath) + } + + if (reRequireDeps) reRequireDependencies() + }, + /** + * Re-require specified dependencies + */ + reRequireDependencies + } + + const helpers = { + /** + * Mock a function temporarily + * + * Set 'times' parameter to control how many times will the function be invoked + * with the mocked implementation + * + * Use args & results arrays in result object to see the passed in arguments and function return values + * that were collected + */ + hijackFunction( + functionName: F, + implementation: MockedFunc, + params: { times: number } = { times: 1 } + ) { + let { times } = params + if (!isFunction(implementation)) + throw new Error('Implementation must be a function') + + const collectedReturns: Array>> = [] + const collectedArgs: Array>> = [] + + core.enable() + core.mockFunction( + functionName, + function (this: unknown, ...args: Parameters>) { + const returnVal = implementation.apply(this, args) + times-- + + if (times <= 0) { + core.resetMockedFunction(functionName) + } + + collectedArgs.push(args) + collectedReturns.push(returnVal) + + return returnVal + } + ) + + return { + /** + * Arguments that were used to call the mocked function. Each entry in this array is an array of arguments, so use the first array dimension to choose + * the invocation and the 2nd dimension to choose the specific argument. + */ + args: collectedArgs, + /** + * Return values that were returned from the mocked function. + */ + returns: collectedReturns, + /** + * Get the amount of invocations + */ + length: () => collectedArgs.length + } + } + } + + return { + ...core, + ...helpers + } +} + +export type MockApiType = ReturnType + +/** + * Create global mock. Essentially the same as mockRequireModule() but simplified + * with safeguards so that you can't destroy it and break it in other tests + * + * Note: Global mocks should be registered in test/hooks.js before everything else! + */ +export function createGlobalMock>( + modulePath: string +) { + const globalMock = mockRequireModule([modulePath], [], { + preventDestroy: true + }) + const { hijackFunction, resetMockedFunctions } = globalMock + + return { + hijackFunction, + resetMockedFunctions + } +} diff --git a/packages/server/test/mocks/global.ts b/packages/server/test/mocks/global.ts new file mode 100644 index 000000000..54fe13856 --- /dev/null +++ b/packages/server/test/mocks/global.ts @@ -0,0 +1,10 @@ +import { createGlobalMock } from '@/test/mockHelper' + +/** + * Global mocks that can be re-used. Remember to .enable() before use and .disable() + * after use to ensure that other tests work with the real module. + */ + +export const EmailSendingServiceMock = createGlobalMock< + typeof import('@/modules/emails/services/sending') +>('@/modules/emails/services/sending') diff --git a/packages/server/test/notificationsHelper.ts b/packages/server/test/notificationsHelper.ts new file mode 100644 index 000000000..9c4c0ee79 --- /dev/null +++ b/packages/server/test/notificationsHelper.ts @@ -0,0 +1,167 @@ +import { getQueue, NotificationJobResult } from '@/modules/notifications/services/queue' +import { EventEmitter } from 'events' +import { CompletedEventCallback, FailedEventCallback, JobId } from 'bull' +import { pick } from 'lodash' + +type AckEvent = { + result?: NotificationJobResult + err?: Error + jobId: JobId +} + +const NEW_ACK_EVENT = 'new-ack' + +export function buildNotificationsStateTracker() { + const queue = getQueue() + const localEvents = new EventEmitter() + + const ackHandler = (e: AckEvent) => { + collectedAcks.set(e.jobId, e) + + // Emit event to waitForAck promise handlers + localEvents.emit(NEW_ACK_EVENT, e) + } + + const completedHandler: CompletedEventCallback = (job, result) => { + ackHandler({ result, jobId: job.id }) + } + const failedHandler: FailedEventCallback = (job, err) => { + ackHandler({ err, jobId: job.id }) + } + + queue.on('completed', completedHandler) + queue.on('failed', failedHandler) + + const collectedAcks = new Map() + + return { + /** + * Quit listening for notification acknowledgements + */ + destroy: () => { + queue.removeListener('completed', completedHandler) + queue.removeListener('failed', failedHandler) + localEvents.removeAllListeners() + }, + + /** + * Reset/clear collected data + */ + reset: () => { + collectedAcks.clear() + }, + + /** + * Wait for an acknowledgement of a specific msg + */ + waitForMsgAck: async (msgId: JobId, timeout = 2000) => { + let timeoutRef: NodeJS.Timer + let eventEmitterHandler: (e: AckEvent) => void + return new Promise((resolve, reject) => { + // Set ack cb for notifications event handler + eventEmitterHandler = (e) => { + if (e.jobId === msgId) return resolve(e) + } + localEvents.on(NEW_ACK_EVENT, eventEmitterHandler) + + // Do we have it already? + const event = collectedAcks.get(msgId) + if (event) { + return resolve(event) + } + + // Set timeout + timeoutRef = setTimeout( + () => reject(new Error('Waiting for notification ack timed out')), + timeout + ) + }).finally(() => { + clearTimeout(timeoutRef) + localEvents.off(NEW_ACK_EVENT, eventEmitterHandler) + }) + }, + + /** + * Wait for an acknowledgement without knowing the msg id + */ + waitForAck: async (predicate?: (e: AckEvent) => boolean, timeout = 2000) => { + let timeoutRef: NodeJS.Timer + let eventEmitterHandler: (e: AckEvent) => void + return new Promise((resolve, reject) => { + // Set ack cb for notifications event handler + eventEmitterHandler = (e) => { + if (!predicate) return resolve(e) + if (predicate && predicate(e)) return resolve(e) + } + localEvents.on(NEW_ACK_EVENT, eventEmitterHandler) + + // Set timeout + timeoutRef = setTimeout( + () => reject(new Error('Waiting for notification ack timed out')), + timeout + ) + }).finally(() => { + clearTimeout(timeoutRef) + localEvents.off(NEW_ACK_EVENT, eventEmitterHandler) + }) + } + } +} + +export type NotificationsStateManager = ReturnType< + typeof buildNotificationsStateTracker +> + +/** + * Purge pre-queued notifications + */ +export async function purgeNotifications() { + const queue = getQueue() + await queue.empty() +} + +/** + * Get current state of the notifications queue & workers + */ +export async function debugJobs() { + const queue = getQueue() + const [waiting, active, delayed, completed, failed, workers] = await Promise.all([ + queue.getWaiting(), + queue.getActive(), + queue.getDelayed(), + queue.getCompleted(), + queue.getFailed(), + queue.getWorkers() + ]) + + const jobCollections = [ + { items: waiting, display: 'Waiting' }, + { items: active, display: 'Active' }, + { items: delayed, display: 'Delayed' }, + { items: completed, display: 'Completed' }, + { items: failed, display: 'Failed' } + ] + + console.log('------------- START debugJobs() --------------') + + for (const { items, display } of jobCollections) { + console.log(`${display}: ` + waiting.length) + console.log(`${display} jobs: `) + for (const job of items) { + console.log( + ` - ${JSON.stringify( + pick(job, [ + 'timestamp', + 'returnvalue', + 'id', + 'processedOn', + 'finishedOn', + 'failedReason' + ]) + )}` + ) + } + } + console.log({ workers }) + console.log('------------- END debugJobs() --------------') +} diff --git a/packages/server/test/plugins/graphql.ts b/packages/server/test/plugins/graphql.ts new file mode 100644 index 000000000..dc0723516 --- /dev/null +++ b/packages/server/test/plugins/graphql.ts @@ -0,0 +1,47 @@ +import { Optional } from '@/modules/shared/helpers/typeHelper' +import { GraphQLResponse } from 'apollo-server-core' + +type ChaiPluginThis> = { + __flags: { + message: Optional + negate: Optional + object: O + } +} + +/** + * Adds various useful assertions for GraphQL API integration tests + */ +const graphqlChaiPlugin: Chai.ChaiPlugin = (_chai, utils) => { + const { Assertion } = _chai + + utils.addMethod( + Assertion.prototype, + 'haveGraphQLErrors', + function (this: ChaiPluginThis, matchMessage?: string) { + const { negate, object } = this.__flags + const { errors } = object + + const errorsArr = errors || [] + + if (negate) { + new Assertion(errorsArr).to.have.lengthOf(0) + } else { + new Assertion(errorsArr).to.have.lengthOf.greaterThanOrEqual(1) + } + + if (matchMessage) { + if (negate) { + new Assertion( + errorsArr.map((e) => e.message.toLowerCase()).join('\n') + ).to.not.contain(matchMessage.toLowerCase()) + } else { + new Assertion( + errorsArr.map((e) => e.message.toLowerCase()).join('\n') + ).to.contain(matchMessage.toLowerCase()) + } + } + } + ) +} +export default graphqlChaiPlugin diff --git a/packages/server/test/serverHelper.ts b/packages/server/test/serverHelper.ts index 6bc8468f7..1701f5ff2 100644 --- a/packages/server/test/serverHelper.ts +++ b/packages/server/test/serverHelper.ts @@ -21,3 +21,15 @@ export function buildAuthenticatedApolloServer( }) }) } + +/** + * Build an unauthenticated ApolloServer instance + */ +export function buildUnauthenticatedApolloServer() { + return buildApolloServer({ + context: () => + addLoadersToCtx({ + auth: false + }) + }) +} diff --git a/packages/server/test/speckle-helpers/activityStreamHelper.ts b/packages/server/test/speckle-helpers/activityStreamHelper.ts new file mode 100644 index 000000000..6cac01be8 --- /dev/null +++ b/packages/server/test/speckle-helpers/activityStreamHelper.ts @@ -0,0 +1,24 @@ +import { StreamActivityRecord } from '@/modules/activitystream/helpers/types' +import { StreamActivity } from '@/modules/core/dbSchema' + +export async function getStreamActivities( + streamId: string, + extraFilters: Partial<{ actionType: string; userId: string }> = {} +): Promise { + const { actionType, userId } = extraFilters + + const q = StreamActivity.knex().where( + StreamActivity.col.streamId, + streamId + ) + + if (actionType) { + q.andWhere(StreamActivity.col.actionType, actionType) + } + + if (userId) { + q.andWhere(StreamActivity.col.userId, userId) + } + + return await q +} diff --git a/packages/server/test/speckle-helpers/streamHelper.js b/packages/server/test/speckle-helpers/streamHelper.js deleted file mode 100644 index 6643663b2..000000000 --- a/packages/server/test/speckle-helpers/streamHelper.js +++ /dev/null @@ -1,22 +0,0 @@ -const { StreamAcl } = require('@/modules/core/dbSchema') - -/** - * Get the role user has for the specified stream - * @param {string} userId - * @param {string} streamId - * @returns {Promise} - */ -async function getUserStreamRole(userId, streamId) { - const entry = await StreamAcl.knex() - .where({ - [StreamAcl.col.resourceId]: streamId, - [StreamAcl.col.userId]: userId - }) - .first() - - return entry?.role || null -} - -module.exports = { - getUserStreamRole -} diff --git a/packages/server/test/speckle-helpers/streamHelper.ts b/packages/server/test/speckle-helpers/streamHelper.ts new file mode 100644 index 000000000..82b53a031 --- /dev/null +++ b/packages/server/test/speckle-helpers/streamHelper.ts @@ -0,0 +1,60 @@ +import { StreamAcl } from '@/modules/core/dbSchema' +import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types' +import { createStream } from '@/modules/core/services/streams' +import { Nullable } from '@/modules/shared/helpers/typeHelper' +import { BasicTestUser } from '@/test/authHelper' +import { omit } from 'lodash' + +export type BasicTestStream = { + name: string + isPublic: boolean + /** + * The ID of the owner user + */ + ownerId: string + /** + * The ID of the stream. Will be filled in by createTestStream(). + */ + id: string +} & Partial + +/** + * Create multiple test streams with their IDs filled in + */ +export async function createTestStreams( + streamOwnerPairs: [BasicTestStream, BasicTestUser][] +) { + await Promise.all(streamOwnerPairs.map((p) => createTestStream(p[0], p[1]))) +} + +/** + * Create basic stream for testing and update streamObj to have a real ID + */ +export async function createTestStream( + streamObj: BasicTestStream, + owner: BasicTestUser +) { + const id = await createStream({ + ...omit(streamObj, ['id', 'ownerId']), + ownerId: owner.id + }) + streamObj.id = id + streamObj.ownerId = owner.id +} + +/** + * Get the role user has for the specified stream + */ +export async function getUserStreamRole( + userId: string, + streamId: string +): Promise> { + const entry = await StreamAcl.knex() + .where({ + [StreamAcl.col.resourceId]: streamId, + [StreamAcl.col.userId]: userId + }) + .first() + + return entry?.role || null +} diff --git a/packages/server/type-augmentations/chai.d.ts b/packages/server/type-augmentations/chai.d.ts new file mode 100644 index 000000000..6035eb795 --- /dev/null +++ b/packages/server/type-augmentations/chai.d.ts @@ -0,0 +1,13 @@ +declare global { + namespace Chai { + interface Assertion { + /** + * Check if GraphQLResponse object has any errors + * @param message Optionally check if any of the errors contain the specified message + */ + haveGraphQLErrors(message?: string): void + } + } +} + +export {} diff --git a/packages/server/type-augmentations/verror.d.ts b/packages/server/type-augmentations/verror.d.ts new file mode 100644 index 000000000..70750091e --- /dev/null +++ b/packages/server/type-augmentations/verror.d.ts @@ -0,0 +1,69 @@ +/** + * Mostly the same as @types/verror:1.10, but adjusted so that it doesn't conflict with the + * new native Error.cause API. + */ + +declare module 'verror' { + declare class VError extends Error { + static VError: typeof VError + + static cause(err: Error): Error | null + static info(err: Error): VError.Info + static fullStack(err: Error): string + static findCauseByName(err: Error, name: string): Error | null + static hasCauseWithName(err: Error, name: string): boolean + static errorFromList(errors: T[]): null | T | VError.MultiError + static errorForEach(err: Error, func: (err: Error) => void): void + + /** + * @deprecated Use `getCause()` from `errorHelper.ts` instead. This prop isn't going to + * work because it's actually a function and it's only marked as an Error so as to not conflict + * with the new native Error.cause API. + */ + cause?: Error + constructor(options: VError.Options | Error, message: string, ...params: any[]) + constructor(message?: string, ...params: any[]) + } + + declare namespace VError { + interface Info { + [key: string]: any + } + + interface Options { + cause?: Error | null | undefined + name?: string | undefined + strict?: boolean | undefined + constructorOpt?(...args: any[]): void + info?: Info | undefined + } + + /* + * SError is like VError, but stricter about types. You cannot pass "null" or + * "undefined" as string arguments to the formatter. Since SError is only a + * different function, not really a different class, we don't set + * SError.prototype.name. + */ + class SError extends VError {} + + /* + * Represents a collection of errors for the purpose of consumers that generally + * only deal with one error. Callers can extract the individual errors + * contained in this object, but may also just treat it as a normal single + * error, in which case a summary message will be printed. + */ + class MultiError extends VError { + constructor(errors: Error[]) + errors(): Error[] + } + + /* + * Like JavaScript's built-in Error class, but supports a "cause" argument which + * is wrapped, not "folded in" as with VError. Accepts a printf-style message. + * The cause argument can be null. + */ + class WError extends VError {} + } + + export = VError +} diff --git a/packages/viewer/package.json b/packages/viewer/package.json index a30fb32da..10b72fefd 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -21,7 +21,7 @@ "dist" ], "engines": { - "node": "^16.0.0" + "node": ">=14.0.0" }, "scripts": { "build": "NODE_ENV=production rollup --config", diff --git a/utils/helm/speckle-server/templates/_helpers.tpl b/utils/helm/speckle-server/templates/_helpers.tpl index 5272acc3c..376455887 100644 --- a/utils/helm/speckle-server/templates/_helpers.tpl +++ b/utils/helm/speckle-server/templates/_helpers.tpl @@ -100,44 +100,177 @@ Creates a network policy egress definition for connecting to Redis Expects the global context "$" to be passed as the parameter */}} {{- define "speckle.networkpolicy.egress.redis" -}} -{{- $port := (default "6379" .Values.redis.networkPolicy.port ) -}} {{- if .Values.redis.networkPolicy.inCluster.enabled -}} -{{ include "speckle.networkpolicy.egress.internal" (dict "podSelector" .Values.redis.networkPolicy.inCluster.podSelector "namespaceSelector" .Values.redis.networkPolicy.inCluster.namespaceSelector "port" $port) }} + {{- $port := (default "6379" .Values.redis.networkPolicy.inCluster.port ) -}} +{{ include "speckle.networkpolicy.egress.internal" (dict "podSelector" .Values.redis.networkPolicy.inCluster.kubernetes.podSelector "namespaceSelector" .Values.redis.networkPolicy.inCluster.kubernetes.namespaceSelector "port" $port) }} {{- else if .Values.redis.networkPolicy.externalToCluster.enabled -}} -{{ include "speckle.networkpolicy.egress.external" (dict "ip" .Values.redis.networkPolicy.externalToCluster.ipv4 "port" $port) }} + {{- $secret := ( include "speckle.getSecret" (dict "secret_key" "redis_url" "context" . ) ) -}} + {{- $domain := ( include "speckle.networkPolicy.domainFromUrl" $secret ) -}} + {{- $port := ( default "6379" ( include "speckle.networkPolicy.portFromUrl" $secret ) ) -}} +{{ include "speckle.networkpolicy.egress.external" (dict "ip" $domain "port" $port) }} {{- end -}} {{- end }} {{/* -Creates a network policy egress definition for connecting to Postgres +Creates a Cilium Network Policy egress definition for connecting to Redis + +Expects the global context "$" to be passed as the parameter +*/}} +{{- define "speckle.networkpolicy.egress.redis.cilium" -}} +{{- if .Values.redis.networkPolicy.inCluster.enabled -}} + {{- $port := (default "6379" .Values.redis.networkPolicy.inCluster.port ) -}} +{{ include "speckle.networkpolicy.egress.internal.cilium" (dict "endpointSelector" .Values.redis.networkPolicy.inCluster.cilium.endpointSelector "serviceSelector" .Values.redis.networkPolicy.inCluster.cilium.serviceSelector "port" $port) }} +{{- else if .Values.redis.networkPolicy.externalToCluster.enabled -}} + {{- $secret := ( include "speckle.getSecret" (dict "secret_key" "redis_url" "context" . ) ) -}} + {{- $domain := ( include "speckle.networkPolicy.domainFromUrl" $secret ) -}} + {{- $port := ( default "6379" ( include "speckle.networkPolicy.portFromUrl" $secret ) ) -}} +{{ include "speckle.networkpolicy.egress.external.cilium" (dict "ip" $domain "port" $port) }} +{{- end -}} +{{- end }} + +{{/* +Creates a Kubernetes Network Policy egress definition for connecting to Postgres */}} {{- define "speckle.networkpolicy.egress.postgres" -}} -{{- $port := (default "5432" .Values.db.networkPolicy.port ) -}} {{- if .Values.db.networkPolicy.inCluster.enabled -}} -{{ include "speckle.networkpolicy.egress.internal" (dict "podSelector" .Values.db.networkPolicy.inCluster.podSelector "namespaceSelector" .Values.db.networkPolicy.inCluster.namespaceSelector "port" $port) }} + {{- $port := (default "5432" .Values.db.networkPolicy.inCluster.port ) -}} +{{ include "speckle.networkpolicy.egress.internal" (dict "podSelector" .Values.db.networkPolicy.inCluster.kubernetes.podSelector "namespaceSelector" .Values.db.networkPolicy.inCluster.kubernetes.namespaceSelector "port" $port) }} {{- else if .Values.db.networkPolicy.externalToCluster.enabled -}} -{{ include "speckle.networkpolicy.egress.external" (dict "ip" .Values.db.networkPolicy.externalToCluster.ipv4 "port" $port) }} + {{- $secret := ( include "speckle.getSecret" (dict "secret_key" "postgres_url" "context" . ) ) -}} + {{- $domain := ( include "speckle.networkPolicy.domainFromUrl" $secret ) -}} + {{- $port := ( default "5432" ( include "speckle.networkPolicy.portFromUrl" $secret ) ) -}} +{{ include "speckle.networkpolicy.egress.external" (dict "ip" $domain "port" $port) }} {{- end -}} {{- end }} {{/* -Creates a network policy egress definition for connecting to Postgres +Creates a Cilium network policy egress definition for connecting to Postgres +*/}} +{{- define "speckle.networkpolicy.egress.postgres.cilium" -}} +{{- if .Values.db.networkPolicy.inCluster.enabled -}} + {{- $port := (default "5432" .Values.db.networkPolicy.inCluster.port ) -}} +{{ include "speckle.networkpolicy.egress.internal.cilium" (dict "endpointSelector" .Values.db.networkPolicy.inCluster.cilium.endpointSelector "serviceSelector" .Values.db.networkPolicy.inCluster.cilium.serviceSelector "port" $port) }} +{{- else if .Values.db.networkPolicy.externalToCluster.enabled -}} + {{- $secret := ( include "speckle.getSecret" (dict "secret_key" "postgres_url" "context" . ) ) -}} + {{- $domain := ( include "speckle.networkPolicy.domainFromUrl" $secret ) -}} + {{- $port := ( default "5432" ( include "speckle.networkPolicy.portFromUrl" $secret ) ) -}} +{{ include "speckle.networkpolicy.egress.external.cilium" (dict "ip" $domain "port" $port) }} +{{- end -}} +{{- end }} + +{{/* +Creates a Kubernetes network policy egress definition for connecting to S3 compatible storage */}} {{- define "speckle.networkpolicy.egress.blob_storage" -}} -{{- $port := (default "443" .Values.s3.networkPolicy.port ) -}} -{{- if .Values.s3.networkPolicy.inCluster.enabled -}} -{{ include "speckle.networkpolicy.egress.internal" (dict "podSelector" .Values.s3.networkPolicy.inCluster.podSelector "namespaceSelector" .Values.s3.networkPolicy.inCluster.namespaceSelector "port" $port) }} -{{- else if .Values.s3.networkPolicy.externalToCluster.enabled -}} - {{- $host := (urlParse .Values.s3.endpoint).host -}} - {{- if (contains ":" $host) -}} - {{- $host = first (mustRegexSplit ":" $host) -}} - {{- end -}} - {{- $ip := "" -}} - {{- if eq (include "speckle.isIPv4" $host) "true" -}} - {{- $ip = $host -}} - {{- end -}} + {{- $port := (default "443" .Values.s3.networkPolicy.port ) -}} + {{- if .Values.s3.networkPolicy.inCluster.enabled -}} +{{ include "speckle.networkpolicy.egress.internal" (dict "podSelector" .Values.s3.networkPolicy.inCluster.kubernetes.podSelector "namespaceSelector" .Values.s3.networkPolicy.inCluster.kubernetes.namespaceSelector "port" $port) }} + {{- else if .Values.s3.networkPolicy.externalToCluster.enabled -}} + {{- $ip := ( include "speckle.networkPolicy.domainFromUrl" .Values.s3.endpoint ) -}} {{ include "speckle.networkpolicy.egress.external" (dict "ip" $ip "port" $port) }} -{{- end -}} + {{- end -}} +{{- end }} + +{{/* +Creates a Cilium Network Policy egress definition for connecting to S3 compatible storage +*/}} +{{- define "speckle.networkpolicy.egress.blob_storage.cilium" -}} + {{- $port := (default "443" .Values.s3.networkPolicy.port ) -}} + {{- if .Values.s3.networkPolicy.inCluster.enabled -}} +{{ include "speckle.networkpolicy.egress.internal.cilium" (dict "endpointSelector" .Values.s3.networkPolicy.inCluster.cilium.endpointSelector "serviceSelector" .Values.s3.networkPolicy.inCluster.cilium.serviceSelector "port" $port) }} + {{- else if .Values.s3.networkPolicy.externalToCluster.enabled -}} + {{- $host := ( include "speckle.networkPolicy.domainFromUrl" .Values.s3.endpoint ) -}} +{{ include "speckle.networkpolicy.egress.external.cilium" (dict "ip" $host "port" $port) }} + {{- end -}} +{{- end }} + +{{/* +Creates a Kubernetes Network Policy egress definition for connecting to the email server + +Params: + - context - Required, global context should be provided +*/}} +{{- define "speckle.networkpolicy.egress.email" -}} + {{- $port := (default "443" .Values.server.email.port ) -}} + {{- if .Values.server.email.networkPolicy.inCluster.enabled -}} +{{ include "speckle.networkpolicy.egress.internal" (dict "podSelector" .Values.server.email.networkPolicy.inCluster.kubernetes.podSelector "namespaceSelector" .Values.server.email.networkPolicy.inCluster.kubernetes.namespaceSelector "port" $port) }} + {{- else if .Values.server.email.networkPolicy.externalToCluster.enabled -}} +{{ include "speckle.networkpolicy.egress.external" (dict "ip" .Values.server.email.host "port" $port) }} + {{- end -}} +{{- end }} + +{{/* +Creates a Cilium Network Policy egress definition for connecting to an email server + +Expects the global context "$" to be passed as the parameter +*/}} +{{- define "speckle.networkpolicy.egress.email.cilium" -}} + {{- $port := (default "443" .Values.server.email.port ) -}} + {{- if .Values.server.email.networkPolicy.inCluster.enabled -}} +{{ include "speckle.networkpolicy.egress.internal.cilium" (dict "endpointSelector" .Values.server.email.networkPolicy.inCluster.cilium.endpointSelector "serviceSelector" .Values.server.email.networkPolicy.inCluster.cilium.serviceSelector "port" $port) }} + {{- else if .Values.server.email.networkPolicy.externalToCluster.enabled -}} +{{ include "speckle.networkpolicy.egress.external.cilium" (dict "ip" .Values.server.email.host "port" $port) }} + {{- end -}} +{{- end }} + +{{/* +Creates a DNS match pattern for discovering the postgres IP + +Usage: +{{ include "speckle.networkpolicy.dns.postgres.cilium" $ }} + +Params: + - context - Required, global context should be provided. +*/}} +{{- define "speckle.networkpolicy.dns.postgres.cilium" -}} +{{- $secret := ( include "speckle.getSecret" (dict "secret_key" "postgres_url" "context" . ) ) -}} +{{- $domain := ( include "speckle.networkPolicy.domainFromUrl" $secret ) -}} + {{- if (and .Values.db.networkPolicy.externalToCluster.enabled ( ne ( include "speckle.isIPv4" $domain ) "true" ) ) -}} +{{ include "speckle.networkpolicy.matchNameOrPattern" $domain }} + {{- end }} +{{- end }} + +{{/* +Creates a DNS match pattern for discovering redis store IP + +Usage: +{{ include "speckle.networkpolicy.dns.redis.cilium" $ }} + +Params: + - context - Required, global context should be provided. +*/}} +{{- define "speckle.networkpolicy.dns.redis.cilium" -}} +{{- $secret := ( include "speckle.getSecret" (dict "secret_key" "redis_url" "context" . ) ) -}} +{{- $domain := ( include "speckle.networkPolicy.domainFromUrl" $secret ) -}} + {{- if (and .Values.redis.networkPolicy.externalToCluster.enabled ( ne ( include "speckle.isIPv4" $domain ) "true" ) ) -}} +{{ include "speckle.networkpolicy.matchNameOrPattern" $domain }} + {{- end }} +{{- end }} + +{{/* +Creates a DNS match pattern for discovering blob storage IP +*/}} +{{- define "speckle.networkpolicy.dns.blob_storage.cilium" -}} +{{- $domain := ( include "speckle.networkPolicy.domainFromUrl" .Values.s3.endpoint ) -}} + {{- if ne (include "speckle.isIPv4" $domain ) "true" -}} +{{ include "speckle.networkpolicy.matchNameOrPattern" $domain }} + {{- end }} +{{- end }} + +{{/* +Creates a DNS match pattern for discovering email server IP + +Usage: +{{ include "speckle.networkpolicy.dns.email.cilium" $ }} + +Params: + - context - Required, global context should be provided. +*/}} +{{- define "speckle.networkpolicy.dns.email.cilium" -}} +{{- $domain := .Values.server.email.host -}} + {{- if (and .Values.server.email.networkPolicy.externalToCluster.enabled ( ne ( include "speckle.isIPv4" $domain ) "true" ) ) -}} +{{ include "speckle.networkpolicy.matchNameOrPattern" $domain }} + {{- end }} {{- end }} {{/* @@ -147,7 +280,7 @@ Usage: {{ include "speckle.networkpolicy.egress.external" (dict "ip" "" "port" "6379") }} Params: - - ip - String - Optional - If the IP is not known, then egress is allowed to 0.0.0.0/0. + - ip - String - Optional - IP or Domain of the endpoint to allow egress to. Can provide either ip, fqdn or neither. If neither fqdn or ip is provided then egress is allowed to 0.0.0.0/0 (i.e. everywhere!) - port - String - Required Limitations: @@ -161,7 +294,7 @@ Limitations: {{- end -}} - to: - ipBlock: - {{- if .ip }} + {{- if ( eq ( include "speckle.isIPv4" .ip ) "true" ) }} cidr: {{ printf "%s/32" .ip }} {{- else }} # Kubernetes network policy does not support fqdn, so we have to allow egress anywhere @@ -174,11 +307,59 @@ Limitations: - port: {{ printf "%s" .port }} {{- end }} +{{/* +Creates a Cilium network policy egress definition for connecting to an external Layer 3/Layer 4 endpoint i.e. ip:port + +Usage: +{{ include "speckle.networkpolicy.egress.external.cilium" (dict "ip" "" "port" "6379") }} + +Params: + - ip - String - Optional - IP or Domain of the endpoint to allow egress to. Can provide either ip, fqdn or neither. If neither fqdn or ip is provided then egress is allowed to 0.0.0.0/0 (i.e. everywhere!) + - port - String - Required + +Limitations: + - IP is limited to IPv4 due to Kubernetes use of IPv4 CIDR +*/}} +{{- define "speckle.networkpolicy.egress.external.cilium" -}} +{{- if not .port -}} + {{- printf "\nNETWORKPOLICY ERROR: The port was not provided \"%s\"\n" .port | fail -}} +{{- end -}} +{{- if ( eq ( include "speckle.isIPv4" .ip ) "true" ) }} +- toCIDR: + - {{ printf "%s/32" .ip }} +{{- else if .ip }} +- toFQDNs: +{{ include "speckle.networkpolicy.matchNameOrPattern" .ip | indent 4 }} +{{- else }} +- toCIDRSet: + # Kubernetes network policy does not support fqdn, so we have to allow egress anywhere + - cidr: 0.0.0.0/0 + # except to kubernetes pods or services + except: + - 10.0.0.0/8 +{{- end }} + toPorts: + - ports: + - port: {{ printf "%s" .port | quote }} + protocol: TCP +{{- end }} + +{{- define "speckle.networkpolicy.matchNameOrPattern" -}} +{{- if not . -}} + {{- printf "\nNETWORKPOLICY ERROR: The name or glob pattern was not provided \"%s\"\n" . | fail -}} +{{- end -}} + {{- if ( contains "*" . ) }} +- matchPattern: {{ printf "%s" . }} + {{- else }} +- matchName: {{ printf "%s" . }} + {{- end }} +{{- end }} + {{/* Creates a network policy egress definition for connecting to a pod within the cluster Usage: -{{ include "speckle.networkpolicy.egress.internal" (dict "podSelectorLabels" {matchLabels.name=redis} "namespaceSelector" {matchLabels.name=redis} "port" "6379") }} +{{ include "speckle.networkpolicy.egress.internal" (dict "podSelector" {matchLabels.name=redis} "namespaceSelector" {matchLabels.name=redis} "port" "6379") }} Params: - podSelector - Object - Required @@ -205,6 +386,43 @@ Params: - port: {{ printf "%s" .port }} {{- end }} +{{/* +Creates a cilium network policy egress definition for connecting to an endpoint (pod or kubernetes endpoint) or service within the cluster + +Usage: +{{ include "speckle.networkpolicy.egress.internal.cilium" (dict "endpointSelector" {matchLabels.name=redis matchLabels."io.kubernetes.pod.namespace.labels.name"=speckle} "serviceSelector" "" "port" "6379") }} + +Params: + - endpointSelector - Object - One of endpointSelector or serviceSelector is required. + - serviceSelector - Object - One of endpointSelector or serviceSelector is required. + - port - String - Required + +*/}} +{{- define "speckle.networkpolicy.egress.internal.cilium" -}} +{{- if not .endpointSelector -}} + {{- printf "\nNETWORKPOLICY ERROR: The Endpoint selector was not provided\n" | fail -}} +{{- end -}} +{{- if not .port -}} + {{- printf "\nNETWORKPOLICY ERROR: The port was not provided \"%s\"\n" .port | fail -}} +{{- end -}} +{{- if .endpointSelector }} +- toEndpoints: +{{ .endpointSelector | toYaml | indent 4 }} + toPorts: + - ports: + - port: {{ printf "%s" .port | quote }} + protocol: TCP +{{- end }} +{{- if .serviceSelector }} +- toServices: +{{ .serviceSelector | toYaml | indent 4 }} + toPorts: + - ports: + - port: {{ printf "%s" .port | quote }} + protocol: TCP +{{- end }} +{{- end }} + {{/* Tries to determine if a given string is a valid IP address Usage: @@ -221,6 +439,32 @@ Params: {{- end -}} {{- end -}} +{{/* +Extracts the domain name from a url +*/}} +{{- define "speckle.networkPolicy.domainFromUrl" -}} + {{- if not . -}} + {{- printf "\nERROR: The url was not provided as the context \"%s\"\n" . | fail -}} + {{- end -}} + {{- $host := ( urlParse . ).host -}} + {{- if (contains ":" $host) -}} + {{- $host = first (mustRegexSplit ":" $host -1) -}} + {{- end -}} +{{ printf "%s" $host }} +{{- end }} + +{{/* +Extracts the port from a url +*/}} +{{- define "speckle.networkPolicy.portFromUrl" -}} + {{- if not . -}} + {{- printf "\nERROR: The url was not provided as the context \"%s\"\n" . | fail -}} + {{- end -}} + {{- $host := ( urlParse . ).host -}} + {{- if (contains ":" $host) -}} +{{ printf "%s" ( index (mustRegexSplit ":" $host -1) 1 ) }} + {{- end -}} +{{- end }} {{/* Renders a value that contains template. Usage: @@ -233,3 +477,42 @@ Usage: {{- tpl (.value | toYaml) .context }} {{- end }} {{- end -}} + +{{/* +Selector labels for Prometheus +*/}} +{{- define "speckle.prometheus.selectorLabels" -}} +{{ include "speckle.prometheus.selectorLabels.release" . }} +io.kubernetes.pod.namespace: {{ default .Values.namespace .Values.prometheusMonitoring.namespace }} +{{- end }} + +{{/* +Selector labels for Prometheus release +*/}} +{{- define "speckle.prometheus.selectorLabels.release" -}} +prometheus: {{ default "kube-prometheus-stack" .Values.prometheusMonitoring.release }}-prometheus +{{- end }} + +{{/* +Ingress pod selector +*/}} +{{- define "speckle.ingress.selector.pod" -}} +app.kubernetes.io/name: {{ .Values.ingress.controllerName }} +{{- end }} + +{{/* +Retrieves an existing secret + +Usage: +{{ include "speckle.getSecret" (dict "secret_key" "postgres_url" "context" $ )}} + +Params: + - secret_key - Required, the key within the secret. + - context - Required, must be global context. Values of global context must include 'namespace' and 'secretName' keys. +*/}} +{{- define "speckle.getSecret" -}} +{{- $secretResource := (lookup "v1" "Secret" .context.Values.namespace .context.Values.secretName ) -}} +{{- $secret := ( index $secretResource.data .secret_key ) -}} +{{- $secretDecoded := (b64dec $secret) -}} +{{- printf "%s" $secretDecoded }} +{{- end }} diff --git a/utils/helm/speckle-server/templates/fileimport_service/deployment.yml b/utils/helm/speckle-server/templates/fileimport_service/deployment.yml index aac7b446a..4c9acf3b9 100644 --- a/utils/helm/speckle-server/templates/fileimport_service/deployment.yml +++ b/utils/helm/speckle-server/templates/fileimport_service/deployment.yml @@ -12,6 +12,8 @@ spec: matchLabels: app: speckle-fileimport-service project: speckle-server + strategy: + type: RollingUpdate template: metadata: labels: @@ -64,7 +66,7 @@ spec: env: - name: SPECKLE_SERVER_URL - value: "http://speckle-server:3000" + value: {{ printf "http://%s:%s" ( include "server.service.fqdn" $ ) ( include "server.port" $ ) }} - name: PG_CONNECTION_STRING valueFrom: diff --git a/utils/helm/speckle-server/templates/fileimport_service/networkpolicy.cilium.yml b/utils/helm/speckle-server/templates/fileimport_service/networkpolicy.cilium.yml new file mode 100644 index 000000000..a56f8e596 --- /dev/null +++ b/utils/helm/speckle-server/templates/fileimport_service/networkpolicy.cilium.yml @@ -0,0 +1,52 @@ +{{- if (and (.Values.fileimport_service.networkPolicy.enabled) (eq .Values.networkPlugin.type "cilium")) -}} +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ include "fileimport_service.name" $ }} + namespace: {{ .Values.namespace }} + labels: +{{ include "fileimport_service.labels" . | indent 4 }} +spec: + endpointSelector: + matchLabels: +{{ include "fileimport_service.selectorLabels" . | indent 6 }} +{{- if .Values.enable_prometheus_monitoring }} + ingress: + - fromEndpoints: + - matchLabels: +{{ include "speckle.prometheus.selectorLabels" $ | indent 12 }} + toPorts: + - ports: + - port: "metrics" + protocol: TCP +{{- else }} + ingressDeny: + - fromEntities: + - "all" +{{- end }} + egress: + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: kube-system + k8s-app: kube-dns + toPorts: + - ports: + - port: "53" + protocol: UDP + rules: + dns: + - matchName: {{ include "server.service.fqdn" $ }} +{{ include "speckle.networkpolicy.dns.postgres.cilium" $ | indent 14 }} + # allow egress to speckle-server + - toServices: + - k8sServiceSelector: + namespace: {{ printf "%s" .Values.namespace }} + selector: + matchLabels: +{{ include "server.selectorLabels" $ | indent 16 }} + toPorts: + - ports: + - port: {{ printf "%s" ( include "server.port" $ | quote ) }} + # postgres +{{ include "speckle.networkpolicy.egress.postgres.cilium" $ | indent 4 }} +{{- end }} diff --git a/utils/helm/speckle-server/templates/fileimport_service/networkpolicy.yml b/utils/helm/speckle-server/templates/fileimport_service/networkpolicy.kubernetes.yml similarity index 78% rename from utils/helm/speckle-server/templates/fileimport_service/networkpolicy.yml rename to utils/helm/speckle-server/templates/fileimport_service/networkpolicy.kubernetes.yml index e7a92305b..72cf2de9f 100644 --- a/utils/helm/speckle-server/templates/fileimport_service/networkpolicy.yml +++ b/utils/helm/speckle-server/templates/fileimport_service/networkpolicy.kubernetes.yml @@ -1,4 +1,4 @@ -{{- if .Values.fileimport_service.networkPolicy.enabled -}} +{{- if (and (.Values.fileimport_service.networkPolicy.enabled) (eq .Values.networkPlugin.type "kubernetes")) -}} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -13,6 +13,7 @@ spec: policyTypes: - Egress - Ingress +{{- if .Values.enable_prometheus_monitoring }} ingress: - from: - namespaceSelector: @@ -20,9 +21,13 @@ spec: kubernetes.io/metadata.name: {{ default .Values.namespace .Values.prometheusMonitoring.namespace }} podSelector: matchLabels: - prometheus: {{ default "kube-prometheus-stack" .Values.prometheusMonitoring.release }}-prometheus + {{ include "speckle.prometheus.selectorLabels.release" $ | indent 14 }} ports: - port: metrics +{{- else }} + # deny all ingress + ingress: [] +{{- end }} egress: # allow access to DNS - to: diff --git a/utils/helm/speckle-server/templates/frontend/deployment.yml b/utils/helm/speckle-server/templates/frontend/deployment.yml index 3c5d9096b..d77460814 100644 --- a/utils/helm/speckle-server/templates/frontend/deployment.yml +++ b/utils/helm/speckle-server/templates/frontend/deployment.yml @@ -11,6 +11,8 @@ spec: matchLabels: app: speckle-frontend project: speckle-server + strategy: + type: RollingUpdate template: metadata: labels: diff --git a/utils/helm/speckle-server/templates/frontend/networkpolicy.cilium.yml b/utils/helm/speckle-server/templates/frontend/networkpolicy.cilium.yml new file mode 100644 index 000000000..f6ec58cfc --- /dev/null +++ b/utils/helm/speckle-server/templates/frontend/networkpolicy.cilium.yml @@ -0,0 +1,25 @@ +{{- if (and (.Values.frontend.networkPolicy.enabled) (eq .Values.networkPlugin.type "cilium")) -}} +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ include "frontend.name" $ }} + namespace: {{ .Values.namespace }} + labels: +{{ include "frontend.labels" . | indent 4 }} +spec: + endpointSelector: + matchLabels: +{{ include "frontend.selectorLabels" . | indent 6 }} + ingress: + - fromEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: {{ .Values.ingress.namespace }} +{{ include "speckle.ingress.selector.pod" $ | indent 12 }} + toPorts: + - ports: + - port: "www" + protocol: TCP + egressDeny: + - toEntities: + - "all" +{{- end }} diff --git a/utils/helm/speckle-server/templates/frontend/networkpolicy.yml b/utils/helm/speckle-server/templates/frontend/networkpolicy.kubernetes.yml similarity index 78% rename from utils/helm/speckle-server/templates/frontend/networkpolicy.yml rename to utils/helm/speckle-server/templates/frontend/networkpolicy.kubernetes.yml index 57ba8114a..e62641d94 100644 --- a/utils/helm/speckle-server/templates/frontend/networkpolicy.yml +++ b/utils/helm/speckle-server/templates/frontend/networkpolicy.kubernetes.yml @@ -1,4 +1,4 @@ -{{- if .Values.frontend.networkPolicy.enabled -}} +{{- if (and (.Values.fileimport_service.networkPolicy.enabled) (eq .Values.networkPlugin.type "kubernetes")) -}} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -21,7 +21,7 @@ spec: kubernetes.io/metadata.name: {{ .Values.ingress.namespace }} - podSelector: matchLabels: - app.kubernetes.io/name: {{ .Values.ingress.controllerName }} +{{ include "frontend.ingress.selector.pod" $ | indent 14 }} ports: - port: www egress: [] # block all egress diff --git a/utils/helm/speckle-server/templates/monitoring/deployment.yml b/utils/helm/speckle-server/templates/monitoring/deployment.yml index dc4c7acf3..eacc4dd5d 100644 --- a/utils/helm/speckle-server/templates/monitoring/deployment.yml +++ b/utils/helm/speckle-server/templates/monitoring/deployment.yml @@ -11,6 +11,8 @@ spec: matchLabels: app: speckle-monitoring project: speckle-server + strategy: + type: RollingUpdate template: metadata: labels: diff --git a/utils/helm/speckle-server/templates/monitoring/networkpolicy.cilium.yml b/utils/helm/speckle-server/templates/monitoring/networkpolicy.cilium.yml new file mode 100644 index 000000000..f3db42da2 --- /dev/null +++ b/utils/helm/speckle-server/templates/monitoring/networkpolicy.cilium.yml @@ -0,0 +1,41 @@ +{{- if (and (.Values.monitoring.networkPolicy.enabled) (eq .Values.networkPlugin.type "cilium")) -}} +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ include "monitoring.name" $ }} + namespace: {{ .Values.namespace }} + labels: +{{ include "monitoring.labels" . | indent 4 }} +spec: + endpointSelector: + matchLabels: +{{ include "monitoring.selectorLabels" . | indent 6 }} +{{- if .Values.enable_prometheus_monitoring }} + ingress: + - fromEndpoints: + - matchLabels: +{{ include "speckle.prometheus.selectorLabels" $ | indent 12 }} + toPorts: + - ports: + - port: "metrics" + protocol: TCP +{{- else }} + ingressDeny: + - fromEntities: + - "all" +{{- end }} + egress: + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: kube-system + k8s-app: kube-dns + toPorts: + - ports: + - port: "53" + protocol: UDP + rules: + dns: +{{ include "speckle.networkpolicy.dns.postgres.cilium" $ | indent 14 }} + # postgres +{{ include "speckle.networkpolicy.egress.postgres.cilium" $ | indent 4 }} +{{- end }} diff --git a/utils/helm/speckle-server/templates/monitoring/networkpolicy.yml b/utils/helm/speckle-server/templates/monitoring/networkpolicy.kubernetes.yml similarity index 75% rename from utils/helm/speckle-server/templates/monitoring/networkpolicy.yml rename to utils/helm/speckle-server/templates/monitoring/networkpolicy.kubernetes.yml index 307bd60fd..aef973fc2 100644 --- a/utils/helm/speckle-server/templates/monitoring/networkpolicy.yml +++ b/utils/helm/speckle-server/templates/monitoring/networkpolicy.kubernetes.yml @@ -1,4 +1,4 @@ -{{- if .Values.monitoring.networkPolicy.enabled -}} +{{- if (and (.Values.fileimport_service.networkPolicy.enabled) (eq .Values.networkPlugin.type "kubernetes")) -}} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -13,6 +13,7 @@ spec: policyTypes: - Egress - Ingress +{{- if .Values.enable_prometheus_monitoring }} ingress: - from: - namespaceSelector: @@ -20,9 +21,13 @@ spec: kubernetes.io/metadata.name: {{ default .Values.namespace .Values.prometheusMonitoring.namespace }} podSelector: matchLabels: - prometheus: {{ default "kube-prometheus-stack" .Values.prometheusMonitoring.release }}-prometheus + {{ include "speckle.prometheus.selectorLabels.release" $ | indent 14 }} ports: - port: metrics +{{- else }} + # deny all ingress + ingress: [] +{{- end }} egress: # allow access to DNS - to: diff --git a/utils/helm/speckle-server/templates/preview_service/deployment.yml b/utils/helm/speckle-server/templates/preview_service/deployment.yml index ad98d3eed..145eb5e33 100644 --- a/utils/helm/speckle-server/templates/preview_service/deployment.yml +++ b/utils/helm/speckle-server/templates/preview_service/deployment.yml @@ -11,6 +11,8 @@ spec: matchLabels: app: speckle-preview-service project: speckle-server + strategy: + type: RollingUpdate template: metadata: labels: diff --git a/utils/helm/speckle-server/templates/preview_service/networkpolicy.cilium.yml b/utils/helm/speckle-server/templates/preview_service/networkpolicy.cilium.yml new file mode 100644 index 000000000..f09991e68 --- /dev/null +++ b/utils/helm/speckle-server/templates/preview_service/networkpolicy.cilium.yml @@ -0,0 +1,41 @@ +{{- if (and (.Values.preview_service.networkPolicy.enabled) (eq .Values.networkPlugin.type "cilium")) -}} +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ include "preview_service.name" $ }} + namespace: {{ .Values.namespace }} + labels: +{{ include "preview_service.labels" . | indent 4 }} +spec: + endpointSelector: + matchLabels: +{{ include "preview_service.selectorLabels" . | indent 6 }} +{{- if .Values.enable_prometheus_monitoring }} + ingress: + - fromEndpoints: + - matchLabels: +{{ include "speckle.prometheus.selectorLabels" $ | indent 12 }} + toPorts: + - ports: + - port: "metrics" + protocol: TCP +{{- else }} + ingressDeny: + - fromEntities: + - "all" +{{- end }} + egress: + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: kube-system + k8s-app: kube-dns + toPorts: + - ports: + - port: "53" + protocol: UDP + rules: + dns: +{{ include "speckle.networkpolicy.dns.postgres.cilium" $ | indent 14 }} + # postgres +{{ include "speckle.networkpolicy.egress.postgres.cilium" $ | indent 4 }} +{{- end }} diff --git a/utils/helm/speckle-server/templates/preview_service/networkpolicy.yml b/utils/helm/speckle-server/templates/preview_service/networkpolicy.kubernetes.yml similarity index 75% rename from utils/helm/speckle-server/templates/preview_service/networkpolicy.yml rename to utils/helm/speckle-server/templates/preview_service/networkpolicy.kubernetes.yml index 76ad67b9e..513f41557 100644 --- a/utils/helm/speckle-server/templates/preview_service/networkpolicy.yml +++ b/utils/helm/speckle-server/templates/preview_service/networkpolicy.kubernetes.yml @@ -1,4 +1,4 @@ -{{- if .Values.preview_service.networkPolicy.enabled -}} +{{- if (and (.Values.fileimport_service.networkPolicy.enabled) (eq .Values.networkPlugin.type "kubernetes")) -}} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -13,6 +13,7 @@ spec: policyTypes: - Egress - Ingress +{{- if .Values.enable_prometheus_monitoring }} ingress: - from: - namespaceSelector: @@ -20,9 +21,13 @@ spec: kubernetes.io/metadata.name: {{ default .Values.namespace .Values.prometheusMonitoring.namespace }} podSelector: matchLabels: - prometheus: {{ default "kube-prometheus-stack" .Values.prometheusMonitoring.release }}-prometheus + {{ include "speckle.prometheus.selectorLabels.release" $ | indent 14 }} ports: - port: metrics +{{- else }} + # deny all ingress + ingress: [] +{{- end }} egress: # allow access to DNS - to: diff --git a/utils/helm/speckle-server/templates/server/_helpers.tpl b/utils/helm/speckle-server/templates/server/_helpers.tpl index a75b1f094..a0c79e8f5 100644 --- a/utils/helm/speckle-server/templates/server/_helpers.tpl +++ b/utils/helm/speckle-server/templates/server/_helpers.tpl @@ -41,6 +41,20 @@ app.kubernetes.io/name: {{ include "server.name" . }} {{ include "speckle.commonSelectorLabels" . }} {{- end }} +{{/* +Service FQDN +*/}} +{{- define "server.service.fqdn" -}} +{{ printf "%s.%s.svc.cluster.local." (include "server.name" $) .Values.namespace }} +{{- end }} + +{{/* +Server Port +*/}} +{{- define "server.port" -}} +{{ printf "%d" 3000 }} +{{- end }} + {{/* Create the name of the service account to use */}} diff --git a/utils/helm/speckle-server/templates/server/deployment.yml b/utils/helm/speckle-server/templates/server/deployment.yml index 606a90794..152440e0b 100644 --- a/utils/helm/speckle-server/templates/server/deployment.yml +++ b/utils/helm/speckle-server/templates/server/deployment.yml @@ -1,7 +1,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: speckle-server + name: {{ include "server.name" $ }} namespace: {{ .Values.namespace }} labels: {{ include "server.labels" . | indent 4 }} @@ -11,6 +11,8 @@ spec: matchLabels: app: speckle-server project: speckle-server + strategy: + type: RollingUpdate template: metadata: labels: @@ -23,7 +25,7 @@ spec: ports: - name: http - containerPort: 3000 + containerPort: {{ include "server.port" $ }} protocol: TCP resources: @@ -89,7 +91,7 @@ spec: {{- end }} - name: PORT - value: "3000" + value: {{ include "server.port" $ | quote }} - name: DEBUG value: "speckle:*" diff --git a/utils/helm/speckle-server/templates/server/networkpolicy.cilium.yml b/utils/helm/speckle-server/templates/server/networkpolicy.cilium.yml new file mode 100644 index 000000000..5e1dea390 --- /dev/null +++ b/utils/helm/speckle-server/templates/server/networkpolicy.cilium.yml @@ -0,0 +1,169 @@ +{{- if (and (.Values.server.networkPolicy.enabled) (eq .Values.networkPlugin.type "cilium")) -}} +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ include "server.name" $ }} + namespace: {{ .Values.namespace }} + labels: +{{ include "server.labels" . | indent 4 }} +spec: + endpointSelector: + matchLabels: +{{ include "server.selectorLabels" . | indent 6 }} + ingress: + - fromEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: {{ .Values.ingress.namespace }} +{{ include "speckle.ingress.selector.pod" $ | indent 12 }} + toPorts: + - ports: + - port: http + protocol: TCP +{{- if .Values.enable_prometheus_monitoring }} + - fromEndpoints: + - matchLabels: +{{ include "speckle.prometheus.selectorLabels" $ | indent 12 }} + toPorts: + - ports: + - port: http + protocol: TCP +{{- end }} + # ingress from file import service + - fromEndpoints: + - matchLabels: +{{ include "fileimport_service.selectorLabels" $ | indent 12 }} + toPorts: + - ports: + - port: http + protocol: TCP + egress: + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: kube-system + k8s-app: kube-dns + toPorts: + - ports: + - port: "53" + protocol: UDP + rules: + dns: + # TODO: remove egress to domain once https://github.com/specklesystems/speckle-server/issues/959 is fixed + - matchName: {{ .Values.domain }} +{{- if .Values.server.monitoring.apollo.enabled }} + - matchPattern: "*.api.apollographql.com" +{{- end }} +{{- if .Values.server.sentry_dns }} + # DNS lookup for sentry + - matchPattern: "*.ingest.sentry.io" +{{- end }} +{{- if .Values.server.email.enabled }} + # email server +{{ include "speckle.networkpolicy.dns.email.cilium" $ | indent 14 }} +{{- end }} +{{- if .Values.server.auth.google.enabled }} + # google auth + - matchName: 'accounts.google.com' + - matchName: 'www.googleapis.com' +{{- end }} +{{- if .Values.server.auth.github.enabled }} + # github auth + - matchName: 'github.com' + - matchName: 'api.github.com' +{{- end }} +{{- if .Values.server.auth.azure_ad.enabled }} + # azure ad auth + - matchPattern: '*.login.microsoftonline.com' + - matchPattern: '*.aadcdn.msftauth.net' + - matchPattern: '*.logincdn.msftauth.net' + - matchPattern: '*.login.live.com' + - matchPattern: '*.msauth.net' + - matchPattern: '*.aadcdn.microsoftonline-p.com' + - matchPattern: '*.microsoftonline-p.com' + - matchPattern: '*.account.microsoft.com' + - matchPattern: '*.bmx.azure.com' + - matchPattern: '*.subscriptionrp.trafficmanager.net' + - matchPattern: '*.signup.azure.com' + - matchName: 'login.microsoftonline.com' + - matchName: 'login.windows.net' +{{ include "speckle.renderTpl" (dict "value" .Values.server.auth.azure_ad.networkPolicy.domains "context" $ ) | indent 14 }} +{{- end }} +{{ include "speckle.networkpolicy.dns.postgres.cilium" $ | indent 14 }} +{{ include "speckle.networkpolicy.dns.redis.cilium" $ | indent 14 }} +{{ include "speckle.networkpolicy.dns.blob_storage.cilium" $ | indent 14 }} +{{- if .Values.server.monitoring.apollo.enabled }} + - toFQDNs: + - matchPattern: "*.api.apollographql.com" + toPorts: + - ports: + - port: "443" + protocol: TCP +{{- end }} +{{- if .Values.server.sentry_dns }} + # egress to sentry + - toCIDRSet: + - cidr: 34.120.195.249/32 + toPorts: + - ports: + - port: "443" + protocol: TCP +{{- end }} +{{- if .Values.server.email.enabled }} + # email server +{{ include "speckle.networkpolicy.egress.email.cilium" $ | indent 4 }} +{{- end }} +{{- if .Values.server.auth.google.enabled }} + # google auth + - toFQDNs: + - matchName: 'accounts.google.com' + - matchName: 'www.googleapis.com' + toPorts: + - ports: + - port: '443' + protocol: TCP +{{- end }} +{{- if .Values.server.auth.github.enabled }} + # github auth + - toFQDNs: + - matchName: 'github.com' + - matchName: 'api.github.com' + toPorts: + - ports: + - port: '443' + protocol: TCP +{{- end }} +{{- if .Values.server.auth.azure_ad.enabled }} + # azure ad auth + - toFQDNs: + - matchPattern: '*.login.microsoftonline.com' + - matchPattern: '*.aadcdn.msftauth.net' + - matchPattern: '*.logincdn.msftauth.net' + - matchPattern: '*.login.live.com' + - matchPattern: '*.msauth.net' + - matchPattern: '*.aadcdn.microsoftonline-p.com' + - matchPattern: '*.microsoftonline-p.com' + - matchPattern: '*.account.microsoft.com' + - matchPattern: '*.bmx.azure.com' + - matchPattern: '*.subscriptionrp.trafficmanager.net' + - matchPattern: '*.signup.azure.com' + - matchName: 'login.microsoftonline.com' + - matchName: 'login.windows.net' +{{ include "speckle.renderTpl" (dict "value" .Values.server.auth.azure_ad.additional_domains "context" $ ) | indent 8 }} + toPorts: + - port: {{ default 443 .Values.server.auth.azure_ad.port | quote }} + protocol: TCP +{{- end }} + # postgres +{{ include "speckle.networkpolicy.egress.postgres.cilium" $ | indent 4 }} + # redis +{{ include "speckle.networkpolicy.egress.redis.cilium" $ | indent 4 }} + # s3 +{{ include "speckle.networkpolicy.egress.blob_storage.cilium" $ | indent 4 }} + # allow egress to the ingress for speckle-server, so it can call itself + # TODO: remove egress to domain once https://github.com/specklesystems/speckle-server/issues/959 is fixed + - toFQDNs: + - matchName: {{ .Values.domain }} + toPorts: + - ports: + - port: "443" + protocol: TCP +{{- end }} diff --git a/utils/helm/speckle-server/templates/server/networkpolicy.yml b/utils/helm/speckle-server/templates/server/networkpolicy.kubernetes.yml similarity index 70% rename from utils/helm/speckle-server/templates/server/networkpolicy.yml rename to utils/helm/speckle-server/templates/server/networkpolicy.kubernetes.yml index 7bc6ed66e..6a7782b61 100644 --- a/utils/helm/speckle-server/templates/server/networkpolicy.yml +++ b/utils/helm/speckle-server/templates/server/networkpolicy.kubernetes.yml @@ -1,4 +1,4 @@ -{{- if .Values.server.networkPolicy.enabled -}} +{{- if (and (.Values.fileimport_service.networkPolicy.enabled) (eq .Values.networkPlugin.type "kubernetes")) -}} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -23,6 +23,23 @@ spec: ports: - port: 53 protocol: UDP +{{- if .Values.server.monitoring.apollo.enabled }} + - to: + - ipBlock: + cidr: 34.120.83.176/32 + ports: + - port: 443 +{{- end }} +{{- if ( or .Values.server.auth.google.enabled .Values.server.auth.github.enabled .Values.server.auth.azure_ad.enabled ) }} + - to: + - ipBlock: + cidr: 0.0.0.0/0 + # except to kubernetes pods or services + except: + - 10.0.0.0/8 + ports: + - port: 443 +{{- end }} {{- if .Values.server.sentry_dns }} # sentry.io https://docs.sentry.io/product/security/ip-ranges/#event-ingestion - to: @@ -30,6 +47,10 @@ spec: cidr: 34.120.195.249/32 ports: - port: 443 +{{- end }} +{{- if .Values.server.email.enabled }} + # email server +{{ include "speckle.networkpolicy.egress.email" $ | indent 4 }} {{- end }} # redis {{ include "speckle.networkpolicy.egress.redis" $ | indent 4 }} @@ -48,6 +69,7 @@ spec: app.kubernetes.io/name: {{ .Values.ingress.controllerName }} ports: - port: http +{{- if .Values.enable_prometheus_monitoring }} # allow ingress from servicemonitor/prometheus - from: - namespaceSelector: @@ -55,9 +77,10 @@ spec: kubernetes.io/metadata.name: {{ default .Values.namespace .Values.prometheusMonitoring.namespace }} podSelector: matchLabels: - prometheus: {{ default "kube-prometheus-stack" .Values.prometheusMonitoring.release }}-prometheus + {{ include "speckle.prometheus.selectorLabels.release" $ | indent 14 }} ports: - port: http +{{- end }} # allow ingress from the fileimport service - from: - podSelector: diff --git a/utils/helm/speckle-server/templates/server/service.yml b/utils/helm/speckle-server/templates/server/service.yml index eda58029f..1cd2ccb4f 100644 --- a/utils/helm/speckle-server/templates/server/service.yml +++ b/utils/helm/speckle-server/templates/server/service.yml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Service metadata: - name: speckle-server + name: {{ include "server.name" $ }} namespace: {{ .Values.namespace }} labels: {{ include "server.labels" . | indent 4 }} diff --git a/utils/helm/speckle-server/templates/test/_helpers.tpl b/utils/helm/speckle-server/templates/tests/_helpers.tpl similarity index 100% rename from utils/helm/speckle-server/templates/test/_helpers.tpl rename to utils/helm/speckle-server/templates/tests/_helpers.tpl diff --git a/utils/helm/speckle-server/templates/test/deployment.yml b/utils/helm/speckle-server/templates/tests/deployment.yml similarity index 100% rename from utils/helm/speckle-server/templates/test/deployment.yml rename to utils/helm/speckle-server/templates/tests/deployment.yml diff --git a/utils/helm/speckle-server/templates/tests/networkpolicy.cilium.yml b/utils/helm/speckle-server/templates/tests/networkpolicy.cilium.yml new file mode 100644 index 000000000..a17ddf3fe --- /dev/null +++ b/utils/helm/speckle-server/templates/tests/networkpolicy.cilium.yml @@ -0,0 +1,35 @@ +{{- if (and (.Values.test.networkPolicy.enabled) (eq .Values.networkPlugin.type "cilium")) -}} +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ include "test.name" $ }} + namespace: {{ .Values.namespace }} + labels: +{{ include "test.labels" . | indent 4 }} +spec: + endpointSelector: + matchLabels: +{{ include "test.selectorLabels" . | indent 6 }} + ingressDeny: + - fromEntities: + - "all" + egress: + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: kube-system + k8s-app: kube-dns + toPorts: + - ports: + - port: "53" + protocol: UDP + rules: + dns: + - matchName: {{ .Values.domain }} + # allow egress to domain hosting speckle-server + - toFQDNs: + - matchPattern: {{ .Values.domain }} + toPorts: + - ports: + - port: "443" + protocol: TCP +{{- end }} diff --git a/utils/helm/speckle-server/templates/test/networkpolicy.yml b/utils/helm/speckle-server/templates/tests/networkpolicy.kubernetes.yml similarity index 87% rename from utils/helm/speckle-server/templates/test/networkpolicy.yml rename to utils/helm/speckle-server/templates/tests/networkpolicy.kubernetes.yml index be18a04e9..349f5f557 100644 --- a/utils/helm/speckle-server/templates/test/networkpolicy.yml +++ b/utils/helm/speckle-server/templates/tests/networkpolicy.kubernetes.yml @@ -1,4 +1,4 @@ -{{- if and .Values.helm_test_enabled .Values.test.networkPolicy.enabled -}} +{{- if (and (.Values.fileimport_service.networkPolicy.enabled) (eq .Values.networkPlugin.type "kubernetes")) -}} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: diff --git a/utils/helm/speckle-server/templates/test/serviceaccount.yml b/utils/helm/speckle-server/templates/tests/serviceaccount.yml similarity index 100% rename from utils/helm/speckle-server/templates/test/serviceaccount.yml rename to utils/helm/speckle-server/templates/tests/serviceaccount.yml diff --git a/utils/helm/speckle-server/templates/webhook_service/deployment.yml b/utils/helm/speckle-server/templates/webhook_service/deployment.yml index f669e87f8..d3545eebd 100644 --- a/utils/helm/speckle-server/templates/webhook_service/deployment.yml +++ b/utils/helm/speckle-server/templates/webhook_service/deployment.yml @@ -11,6 +11,8 @@ spec: matchLabels: app: speckle-webhook-service project: speckle-server + strategy: + type: RollingUpdate template: metadata: labels: diff --git a/utils/helm/speckle-server/templates/webhook_service/networkpolicy.cilium.yml b/utils/helm/speckle-server/templates/webhook_service/networkpolicy.cilium.yml new file mode 100644 index 000000000..9f78b2a15 --- /dev/null +++ b/utils/helm/speckle-server/templates/webhook_service/networkpolicy.cilium.yml @@ -0,0 +1,46 @@ +{{- if (and (.Values.webhook_service.networkPolicy.enabled) (eq .Values.networkPlugin.type "cilium")) -}} +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ include "webhook_service.name" $ }} + namespace: {{ .Values.namespace }} + labels: +{{ include "webhook_service.labels" . | indent 4 }} +spec: + endpointSelector: + matchLabels: +{{ include "webhook_service.selectorLabels" . | indent 6 }} +{{- if .Values.enable_prometheus_monitoring }} + ingress: + - fromEndpoints: + - matchLabels: +{{ include "speckle.prometheus.selectorLabels" $ | indent 12 }} + toPorts: + - ports: + - port: "metrics" + protocol: TCP +{{- else }} + ingressDeny: + - fromEntities: + - "all" +{{- end }} + egress: + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: kube-system + k8s-app: kube-dns + toPorts: + - ports: + - port: "53" + protocol: UDP + rules: + dns: + # allow dns discoverability for all entities + - matchPattern: "*" +{{ include "speckle.networkpolicy.dns.postgres.cilium" $ | indent 14 }} + # postgres +{{ include "speckle.networkpolicy.egress.postgres.cilium" $ | indent 4 }} + # allow access to all entities outside of the cluster + - toEntities: + - world +{{- end }} diff --git a/utils/helm/speckle-server/templates/webhook_service/networkpolicy.yml b/utils/helm/speckle-server/templates/webhook_service/networkpolicy.kubernetes.yml similarity index 78% rename from utils/helm/speckle-server/templates/webhook_service/networkpolicy.yml rename to utils/helm/speckle-server/templates/webhook_service/networkpolicy.kubernetes.yml index 8055699c9..d2ecdf590 100644 --- a/utils/helm/speckle-server/templates/webhook_service/networkpolicy.yml +++ b/utils/helm/speckle-server/templates/webhook_service/networkpolicy.kubernetes.yml @@ -1,4 +1,4 @@ -{{- if .Values.webhook_service.networkPolicy.enabled -}} +{{- if (and (.Values.fileimport_service.networkPolicy.enabled) (eq .Values.networkPlugin.type "kubernetes")) -}} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -13,6 +13,7 @@ spec: policyTypes: - Egress - Ingress +{{- if .Values.enable_prometheus_monitoring }} ingress: - from: - namespaceSelector: @@ -20,9 +21,13 @@ spec: kubernetes.io/metadata.name: {{ default .Values.namespace .Values.prometheusMonitoring.namespace }} podSelector: matchLabels: - prometheus: {{ default "kube-prometheus-stack" .Values.prometheusMonitoring.release }}-prometheus + {{ include "speckle.prometheus.selectorLabels.release" $ | indent 14 }} ports: - port: metrics +{{- else }} + # deny all ingress + ingress: [] +{{- end }} egress: # webhook can call anything external, but is blocked from egress elsewhere within the cluster - to: diff --git a/utils/helm/speckle-server/values.schema.json b/utils/helm/speckle-server/values.schema.json index 29d3ed487..b748793c5 100644 --- a/utils/helm/speckle-server/values.schema.json +++ b/utils/helm/speckle-server/values.schema.json @@ -27,6 +27,16 @@ "description": "The name of the ClusterIssuer kubernetes resource that provides the SSL Certificate", "default": "letsencrypt-staging" }, + "networkPlugin": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "(Optional) Used to configure which type of NetworkPolicy is deployed. Options are 'kubernetes' or 'cilium'.", + "default": "kubernetes" + } + } + }, "ingress": { "type": "object", "properties": { @@ -108,11 +118,6 @@ "networkPolicy": { "type": "object", "properties": { - "port": { - "type": "string", - "description": "the port on the server providing the Postgres database (default: \"5432\")", - "default": "" - }, "externalToCluster": { "type": "object", "properties": { @@ -120,16 +125,6 @@ "type": "boolean", "description": "If enabled, indicates that the Postgres database is hosted externally to the Kubernetes cluster", "default": true - }, - "host": { - "type": "string", - "description": "The domain name at which the Postgres database is hosted.", - "default": "" - }, - "ipv4": { - "type": "string", - "description": "The IP address at which the Postgres database is hosted", - "default": "" } } }, @@ -141,15 +136,40 @@ "description": "If enabled, indicates that the Postgres database is hosted withing the same Kubernetes cluster in which Speckle will be deployed", "default": false }, - "podSelector": { - "type": "object", - "description": "The pod Selector yaml object used to uniquely select the Postgres database pods within the cluster and given namespace", - "default": {} + "port": { + "type": "string", + "description": "the port on the server providing the Postgres database (default: \"5432\")", + "default": "" }, - "namespaceSelector": { + "kubernetes": { "type": "object", - "description": "The namespace selector yaml object used to uniquely select the namespace in which the Postgres database pods are deployed", - "default": {} + "properties": { + "podSelector": { + "type": "object", + "description": "(Kubernetes Network Policy only) The pod Selector yaml object used to uniquely select the postgres compatible database pods within the cluster and given namespace", + "default": {} + }, + "namespaceSelector": { + "type": "object", + "description": "(Kubernetes Network Policy only) The namespace selector yaml object used to uniquely select the namespace in which the postgres compatible database pods are deployed", + "default": {} + } + } + }, + "cilium": { + "type": "object", + "properties": { + "endpointSelector": { + "type": "object", + "description": "(Cilium Network Policy only) The endpoint selector yaml object used to uniquely select the in-cluster endpoint in which the postgres compatible database pods are deployed", + "default": {} + }, + "serviceSelector": { + "type": "object", + "description": "(Cilium Network Policy only) The service selector yaml object used to uniquely select the in-cluster service providing the postgres compatible database service", + "default": {} + } + } } } } @@ -200,16 +220,6 @@ "type": "boolean", "description": "If enabled, indicates that the s3 compatible storage is hosted externally to the Kubernetes cluster", "default": true - }, - "host": { - "type": "string", - "description": "The domain name at which the s3 compatible storage is hosted.", - "default": "" - }, - "ipv4": { - "type": "string", - "description": "The IP address at which the s3 compatible storage is hosted", - "default": "" } } }, @@ -221,15 +231,35 @@ "description": "If enabled, indicates that the s3 compatible storage is hosted withing the same Kubernetes cluster in which Speckle will be deployed", "default": false }, - "podSelector": { + "kubernetes": { "type": "object", - "description": "The pod Selector yaml object used to uniquely select the s3 compatible storage pods within the cluster and given namespace", - "default": {} + "properties": { + "podSelector": { + "type": "object", + "description": "(Kubernetes Network Policy only) The pod Selector yaml object used to uniquely select the s3 compatible storage pods within the cluster and given namespace", + "default": {} + }, + "namespaceSelector": { + "type": "object", + "description": "(Kubernetes Network Policy only) The namespace selector yaml object used to uniquely select the namespace in which the s3 compatible storage pods are deployed", + "default": {} + } + } }, - "namespaceSelector": { + "cilium": { "type": "object", - "description": "The namespace selector yaml object used to uniquely select the namespace in which the s3 compatible storage pods are deployed", - "default": {} + "properties": { + "endpointSelector": { + "type": "object", + "description": "(Cilium Network Policy only) The endpoint selector yaml object used to uniquely select the in-cluster endpoint in which the s3 compatible storage pods are deployed", + "default": {} + }, + "serviceSelector": { + "type": "object", + "description": "(Cilium Network Policy only) The service selector yaml object used to uniquely select the in-cluster service providing the s3 compatible storage service", + "default": {} + } + } } } } @@ -243,11 +273,6 @@ "networkPolicy": { "type": "object", "properties": { - "port": { - "type": "string", - "description": "the port on the server providing the Redis store (default: \"6379\")", - "default": "" - }, "externalToCluster": { "type": "object", "properties": { @@ -255,16 +280,6 @@ "type": "boolean", "description": "If enabled, indicates that the Redis store is hosted externally to the Kubernetes cluster", "default": true - }, - "host": { - "type": "string", - "description": "The domain name at which the Redis store is hosted.", - "default": "" - }, - "ipv4": { - "type": "string", - "description": "The IP address at which the Redis store is hosted", - "default": "" } } }, @@ -276,15 +291,40 @@ "description": "If enabled, indicates that the Redis store is hosted withing the same Kubernetes cluster in which Speckle will be deployed", "default": false }, - "podSelector": { - "type": "object", - "description": "The pod Selector yaml object used to uniquely select the Redis store pods within the cluster and given namespace", - "default": {} + "port": { + "type": "string", + "description": "the port on the server providing the Redis store (default: \"6379\")", + "default": "" }, - "namespaceSelector": { + "kubernetes": { "type": "object", - "description": "The namespace selector yaml object used to uniquely select the namespace in which the Redis store pods are deployed", - "default": {} + "properties": { + "podSelector": { + "type": "object", + "description": "(Kubernetes Network Policy only) The pod Selector yaml object used to uniquely select the redis store pods within the cluster and given namespace", + "default": {} + }, + "namespaceSelector": { + "type": "object", + "description": "(Kubernetes Network Policy only) The namespace selector yaml object used to uniquely select the namespace in which the redis store pods are deployed", + "default": {} + } + } + }, + "cilium": { + "type": "object", + "properties": { + "endpointSelector": { + "type": "object", + "description": "(Cilium Network Policy only) The endpoint selector yaml object used to uniquely select the in-cluster endpoint in which the redis pods are deployed", + "default": {} + }, + "serviceSelector": { + "type": "object", + "description": "(Cilium Network Policy only) The service selector yaml object used to uniquely select the in-cluster service providing the redis store service", + "default": {} + } + } } } } @@ -397,6 +437,61 @@ "type": "string", "description": "The username with which Speckle will authenticate with the email service.", "default": "" + }, + "networkPolicy": { + "type": "object", + "properties": { + "externalToCluster": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "If enabled, indicates that the email server is hosted externally to the Kubernetes cluster", + "default": true + } + } + }, + "inCluster": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "If enabled, indicates that the email server is hosted withing the same Kubernetes cluster in which Speckle will be deployed", + "default": false + }, + "kubernetes": { + "type": "object", + "properties": { + "podSelector": { + "type": "object", + "description": "(Kubernetes Network Policy only) The pod Selector yaml object used to uniquely select the email server pods within the cluster and given namespace", + "default": {} + }, + "namespaceSelector": { + "type": "object", + "description": "(Kubernetes Network Policy only) The namespace selector yaml object used to uniquely select the namespace in which the email server pods are deployed", + "default": {} + } + } + }, + "cilium": { + "type": "object", + "properties": { + "endpointSelector": { + "type": "object", + "description": "(Cilium Network Policy only) The endpoint selector yaml object used to uniquely select the in-cluster endpoint in which the email server pods are deployed", + "default": {} + }, + "serviceSelector": { + "type": "object", + "description": "(Cilium Network Policy only) The service selector yaml object used to uniquely select the in-cluster service providing the email server", + "default": {} + } + } + } + } + } + } } } }, diff --git a/utils/helm/speckle-server/values.yaml b/utils/helm/speckle-server/values.yaml index 7426cf7c7..14ca85382 100644 --- a/utils/helm/speckle-server/values.yaml +++ b/utils/helm/speckle-server/values.yaml @@ -26,6 +26,16 @@ ssl_canonical_url: true ## cert_manager_issuer: letsencrypt-staging +## @section Network Plugin configuration +## @descriptionStart +## This is used to define the type of network policy that is deployed. +## Different Kubernetes Network Plugins or Container Network Interfaces (CNIs) can make use of different types of +## Network Policy. Some of these provide more features than the standard Kubernetes Network Policy. +## @descriptionEnd +networkPlugin: + ## @param networkPlugin.type (Optional) Used to configure which type of NetworkPolicy is deployed. Options are 'kubernetes' or 'cilium'. + type: 'kubernetes' + ## @section Ingress metadata for NetworkPolicy ## @descriptionStart ## This section is ignored unless networkPolicy is enabled for frontend or server. @@ -122,9 +132,6 @@ db: ## @extra db.networkPolicy If networkPolicy is enabled for any service, this provides the NetworkPolicy with the necessary details to allow egress connections to the Postgres database ## networkPolicy: - ## @param db.networkPolicy.port the port on the server providing the Postgres database (default: "5432") - ## - port: '' ## @extra db.networkPolicy.externalToCluster Only required if the Postgres database is not hosted within the Kubernetes cluster in which Speckle will be deployed. ## externalToCluster: @@ -132,16 +139,6 @@ db: ## Only one of externalToCluster or inCluster should be enabled. If both are enabled then inCluster takes precedence and is the only one deployed ## enabled: true - ## @param db.networkPolicy.externalToCluster.host The domain name at which the Postgres database is hosted. - ## This should match the value provided within the connection string. - ## Provide the IP address if available (use the `ipv4` parameter), as the IP address takes precedence. - ## - host: '' - ## @param db.networkPolicy.externalToCluster.ipv4 The IP address at which the Postgres database is hosted - ## This should be an IP address not within the Kubernetes Cluster Pod or Service IP ranges. - ## If both host and ipv4 parameters are provided, ipv4 takes precedence and host is ignored. - ## - ipv4: '' ## @extra db.networkPolicy.inCluster Only required if the Postgres database is hosted within the Kubernetes cluster in which Speckle will be deployed. ## inCluster: @@ -149,18 +146,35 @@ db: ## Only one of externalToCluster or inCluster should be enabled. If both are enabled then inCluster takes precedence and is the only set of egress network policy rules deployed. ## enabled: false - ## @param db.networkPolicy.inCluster.podSelector The pod Selector yaml object used to uniquely select the Postgres database pods within the cluster and given namespace - ## This is a Kubernetes podSelector object - ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors - ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## @param db.networkPolicy.inCluster.port the port on the server providing the Postgres database (default: "5432") ## - podSelector: {} - ## @param db.networkPolicy.inCluster.namespaceSelector The namespace selector yaml object used to uniquely select the namespace in which the Postgres database pods are deployed - ## This is a Kubernetes namespaceSelector object - ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors - ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ - ## - namespaceSelector: {} + port: '' + kubernetes: + ## @param db.networkPolicy.inCluster.kubernetes.podSelector (Kubernetes Network Policy only) The pod Selector yaml object used to uniquely select the postgres compatible database pods within the cluster and given namespace + ## For Kubernetes Network Policies this is a podSelector object. + ## For Cilium Network Policies this is ignored. + ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + podSelector: {} + ## @param db.networkPolicy.inCluster.kubernetes.namespaceSelector (Kubernetes Network Policy only) The namespace selector yaml object used to uniquely select the namespace in which the postgres compatible database pods are deployed + ## This is a Kubernetes namespaceSelector object. + ## For Cilium Network Policies this is ignored + ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + namespaceSelector: {} + cilium: + ## @param db.networkPolicy.inCluster.cilium.endpointSelector (Cilium Network Policy only) The endpoint selector yaml object used to uniquely select the in-cluster endpoint in which the postgres compatible database pods are deployed + ## For Kubernetes Network Policies this is ignored. + ## ref: https://docs.cilium.io/en/v1.9/policy/language/#egress + ## ref: https://github.com/cilium/cilium/blob/master/pkg/policy/api/selector.go + endpointSelector: {} + ## @param db.networkPolicy.inCluster.cilium.serviceSelector (Cilium Network Policy only) The service selector yaml object used to uniquely select the in-cluster service providing the postgres compatible database service + ## For Kubernetes Network Policies this is ignored. + ## ref: https://docs.cilium.io/en/v1.9/policy/language/#egress + ## ref: https://github.com/cilium/cilium/blob/master/pkg/policy/api/service.go + serviceSelector: {} ## @section S3 Compatible Storage ## @descriptionStart @@ -204,16 +218,6 @@ s3: ## Only one of externalToCluster or inCluster should be enabled. If both are enabled then inCluster takes precedence and is the only one deployed ## enabled: true - ## @param s3.networkPolicy.externalToCluster.host The domain name at which the s3 compatible storage is hosted. - ## This should match the value provided within the connection string. - ## Provide the IP address if available (use the `ipv4` parameter), as the IP address takes precedence. - ## - host: '' - ## @param s3.networkPolicy.externalToCluster.ipv4 The IP address at which the s3 compatible storage is hosted - ## This should be an IP address not within the Kubernetes Cluster Pod or Service IP ranges. - ## If both host and ipv4 parameters are provided, ipv4 takes precedence and host is ignored. - ## - ipv4: '' ## @extra s3.networkPolicy.inCluster Only required if the s3 compatible storage is hosted within the Kubernetes cluster in which Speckle will be deployed. ## inCluster: @@ -221,18 +225,32 @@ s3: ## Only one of externalToCluster or inCluster should be enabled. If both are enabled then inCluster takes precedence and is the only set of egress network policy rules deployed. ## enabled: false - ## @param s3.networkPolicy.inCluster.podSelector The pod Selector yaml object used to uniquely select the s3 compatible storage pods within the cluster and given namespace - ## This is a Kubernetes podSelector object - ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors - ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ - ## - podSelector: {} - ## @param s3.networkPolicy.inCluster.namespaceSelector The namespace selector yaml object used to uniquely select the namespace in which the s3 compatible storage pods are deployed - ## This is a Kubernetes namespaceSelector object - ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors - ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ - ## - namespaceSelector: {} + kubernetes: + ## @param s3.networkPolicy.inCluster.kubernetes.podSelector (Kubernetes Network Policy only) The pod Selector yaml object used to uniquely select the s3 compatible storage pods within the cluster and given namespace + ## For Kubernetes Network Policies this is a podSelector object. + ## For Cilium Network Policies this is ignored. + ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + podSelector: {} + ## @param s3.networkPolicy.inCluster.kubernetes.namespaceSelector (Kubernetes Network Policy only) The namespace selector yaml object used to uniquely select the namespace in which the s3 compatible storage pods are deployed + ## This is a Kubernetes namespaceSelector object. + ## For Cilium Network Policies, this is ignored + ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + namespaceSelector: {} + cilium: + ## @param s3.networkPolicy.inCluster.cilium.endpointSelector (Cilium Network Policy only) The endpoint selector yaml object used to uniquely select the in-cluster endpoint in which the s3 compatible storage pods are deployed + ## For Kubernetes Network Policies, this is ignored. + ## ref: https://docs.cilium.io/en/v1.9/policy/language/#egress + ## ref: https://github.com/cilium/cilium/blob/master/pkg/policy/api/selector.go + endpointSelector: {} + ## @param s3.networkPolicy.inCluster.cilium.serviceSelector (Cilium Network Policy only) The service selector yaml object used to uniquely select the in-cluster service providing the s3 compatible storage service + ## For Kubernetes Network Policies this is ignored. + ## ref: https://docs.cilium.io/en/v1.9/policy/language/#egress + ## ref: https://github.com/cilium/cilium/blob/master/pkg/policy/api/service.go + serviceSelector: {} ## @section Redis Store ## @descriptionStart @@ -246,9 +264,6 @@ redis: ## @extra redis.networkPolicy If networkPolicy is enabled for Speckle server, this provides the NetworkPolicy with the necessary details to allow egress connections to the Redis store ## networkPolicy: - ## @param redis.networkPolicy.port the port on the server providing the Redis store (default: "6379") - ## - port: '' ## @extra redis.networkPolicy.externalToCluster Only required if the Redis store is not hosted within the Kubernetes cluster in which Speckle will be deployed. ## externalToCluster: @@ -256,16 +271,6 @@ redis: ## Only one of externalToCluster or inCluster should be enabled. If both are enabled then inCluster takes precedence and is the only one deployed ## enabled: true - ## @param redis.networkPolicy.externalToCluster.host The domain name at which the Redis store is hosted. - ## This should match the value provided within the connection string. - ## Provide the IP address if available (use the `ipv4` parameter), as the IP address takes precedence. - ## - host: '' - ## @param redis.networkPolicy.externalToCluster.ipv4 The IP address at which the Redis store is hosted - ## This should be an IP address not within the Kubernetes Cluster Pod or Service IP ranges. - ## If both host and ipv4 parameters are provided, ipv4 takes precedence and host is ignored. - ## - ipv4: '' ## @extra redis.networkPolicy.inCluster is only required if the Redis store is hosted within the Kubernetes cluster in which Speckle will be deployed. ## inCluster: @@ -273,18 +278,35 @@ redis: ## Only one of externalToCluster or inCluster should be enabled. If both are enabled then inCluster takes precedence and is the only set of egress network policy rules deployed. ## enabled: false - ## @param redis.networkPolicy.inCluster.podSelector The pod Selector yaml object used to uniquely select the Redis store pods within the cluster and given namespace - ## This is a Kubernetes podSelector object - ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors - ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## @param redis.networkPolicy.inCluster.port the port on the server providing the Redis store (default: "6379") ## - podSelector: {} - ## @param redis.networkPolicy.inCluster.namespaceSelector The namespace selector yaml object used to uniquely select the namespace in which the Redis store pods are deployed - ## This is a Kubernetes namespaceSelector object - ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors - ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ - ## - namespaceSelector: {} + port: '' + kubernetes: + ## @param redis.networkPolicy.inCluster.kubernetes.podSelector (Kubernetes Network Policy only) The pod Selector yaml object used to uniquely select the redis store pods within the cluster and given namespace + ## For Kubernetes Network Policies this is a podSelector object. + ## For Cilium Network Policies this is ignored. + ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + podSelector: {} + ## @param redis.networkPolicy.inCluster.kubernetes.namespaceSelector (Kubernetes Network Policy only) The namespace selector yaml object used to uniquely select the namespace in which the redis store pods are deployed + ## This is a Kubernetes namespaceSelector object. + ## For Cilium Network Policies, this is ignored + ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + namespaceSelector: {} + cilium: + ## @param redis.networkPolicy.inCluster.cilium.endpointSelector (Cilium Network Policy only) The endpoint selector yaml object used to uniquely select the in-cluster endpoint in which the redis pods are deployed + ## For Kubernetes Network Policies, this is ignored. + ## ref: https://docs.cilium.io/en/v1.9/policy/language/#egress + ## ref: https://github.com/cilium/cilium/blob/master/pkg/policy/api/selector.go + endpointSelector: {} + ## @param redis.networkPolicy.inCluster.cilium.serviceSelector (Cilium Network Policy only) The service selector yaml object used to uniquely select the in-cluster service providing the redis store service + ## For Kubernetes Network Policies this is ignored. + ## ref: https://docs.cilium.io/en/v1.9/policy/language/#egress + ## ref: https://github.com/cilium/cilium/blob/master/pkg/policy/api/service.go + serviceSelector: {} ## @section Server ## @descriptionStart @@ -347,6 +369,12 @@ server: ## @param server.auth.azure_ad.client_id This is the ID for Speckle that you have registered with Azure ## client_id: '' + ## @param server.auth.azure_ad.additional_domains List of `matchName` or `matchPattern` maps for domains that should be allow-listed for egress in Network Policy. https://docs.microsoft.com/en-us/azure/azure-portal/azure-portal-safelist-urls?tabs=public-cloud are enabled by default. + ## + additional_domains: [] + ## @param server.auth.azure_ad.port Port on server to connect to. Used to allow egress in Network Policy. Defaults to 443 + ## + port: 443 ## @extra server.email Speckle can communicate with users via email, providing account verification and notification. ## email: @@ -366,6 +394,49 @@ server: ## Note that the `email_password` is expected to be provided in the Kubernetes Secret with the name provided in the `secretName` parameter. ## username: '' + ## @extra server.email.networkPolicy If networkPolicy is enabled for Speckle server, this provides the Network Policy with the necessary details to allow egress connections to the email server + ## + networkPolicy: + ## @extra server.email.networkPolicy.externalToCluster Only required if the Redis store is not hosted within the Kubernetes cluster in which Speckle will be deployed. + ## + externalToCluster: + ## @param server.email.networkPolicy.externalToCluster.enabled If enabled, indicates that the email server is hosted externally to the Kubernetes cluster + ## Only one of externalToCluster or inCluster should be enabled. If both are enabled then inCluster takes precedence and is the only one deployed + ## + enabled: true + ## @extra server.email.networkPolicy.inCluster is only required if the email server is hosted within the Kubernetes cluster in which Speckle will be deployed. + ## + inCluster: + ## @param server.email.networkPolicy.inCluster.enabled If enabled, indicates that the email server is hosted withing the same Kubernetes cluster in which Speckle will be deployed + ## Only one of externalToCluster or inCluster should be enabled. If both are enabled then inCluster takes precedence and is the only set of egress network policy rules deployed. + ## + enabled: false + kubernetes: + ## @param server.email.networkPolicy.inCluster.kubernetes.podSelector (Kubernetes Network Policy only) The pod Selector yaml object used to uniquely select the email server pods within the cluster and given namespace + ## For Kubernetes Network Policies this is a podSelector object. + ## For Cilium Network Policies this is ignored. + ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + podSelector: {} + ## @param server.email.networkPolicy.inCluster.kubernetes.namespaceSelector (Kubernetes Network Policy only) The namespace selector yaml object used to uniquely select the namespace in which the email server pods are deployed + ## This is a Kubernetes namespaceSelector object. + ## For Cilium Network Policies, this is ignored + ## ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + namespaceSelector: {} + cilium: + ## @param server.email.networkPolicy.inCluster.cilium.endpointSelector (Cilium Network Policy only) The endpoint selector yaml object used to uniquely select the in-cluster endpoint in which the email server pods are deployed + ## For Kubernetes Network Policies, this is ignored. + ## ref: https://docs.cilium.io/en/v1.9/policy/language/#egress + ## ref: https://github.com/cilium/cilium/blob/master/pkg/policy/api/selector.go + endpointSelector: {} + ## @param server.email.networkPolicy.inCluster.cilium.serviceSelector (Cilium Network Policy only) The service selector yaml object used to uniquely select the in-cluster service providing the email server + ## For Kubernetes Network Policies this is ignored. + ## ref: https://docs.cilium.io/en/v1.9/policy/language/#egress + ## ref: https://github.com/cilium/cilium/blob/master/pkg/policy/api/service.go + serviceSelector: {} requests: ## @param server.requests.cpu The CPU that should be available on a node when scheduling this pod. ## ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ diff --git a/utils/test-deployment/run_tests.py b/utils/test-deployment/run_tests.py index 2b78eaff2..24ca7a745 100755 --- a/utils/test-deployment/run_tests.py +++ b/utils/test-deployment/run_tests.py @@ -51,9 +51,12 @@ if len(sys.argv) > 2: if not SERVER_VERSION: SERVER_VERSION = os.getenv('SERVER_VERSION') if SERVER_VERSION: - assert server_info.version == SERVER_VERSION, f"The deployed version {server_info.version} doesn't match the expected {SERVER_VERSION}" - print(f"Server version {SERVER_VERSION} is deployed and available") + if not SERVER_VERSION == 'latest': + assert server_info.version == SERVER_VERSION, f"The deployed version {server_info.version} doesn't match the expected {SERVER_VERSION}" + print(f"Server version {SERVER_VERSION} is deployed and available") + else: + print("Not testing server version, as it was set to 'latest'") else: - print("Not testing server version, since it an expected value was not provided via env var or command-line argument") + print("Not testing server version, as an expected value was not provided via environment variables or command-line argument") print('Deployment tests PASS') diff --git a/workspace.code-workspace b/workspace.code-workspace index 4b6b0b763..e1fcf7e2b 100644 --- a/workspace.code-workspace +++ b/workspace.code-workspace @@ -59,7 +59,8 @@ "search.useParentIgnoreFiles": true, "[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - } + }, + "files.eol": "\n" }, "extensions": { // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. diff --git a/yarn.lock b/yarn.lock index ad0f030b3..72ece4152 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1733,6 +1733,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/helper-validator-identifier@npm:7.18.6" + checksum: e295254d616bbe26e48c196a198476ab4d42a73b90478c9842536cf910ead887f5af6b5c4df544d3052a25ccb3614866fa808dc1e3a5a4291acd444e243c0648 + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.16.7": version: 7.16.7 resolution: "@babel/helper-validator-option@npm:7.16.7" @@ -1775,13 +1782,13 @@ __metadata: linkType: hard "@babel/highlight@npm:^7.10.4": - version: 7.17.12 - resolution: "@babel/highlight@npm:7.17.12" + version: 7.18.6 + resolution: "@babel/highlight@npm:7.18.6" dependencies: - "@babel/helper-validator-identifier": ^7.16.7 + "@babel/helper-validator-identifier": ^7.18.6 chalk: ^2.0.0 js-tokens: ^4.0.0 - checksum: 841a11aa353113bcce662b47085085a379251bf8b09054e37e1e082da1bf0d59355a556192a6b5e9ee98e8ee6f1f2831ac42510633c5e7043e3744dda2d6b9d6 + checksum: 92d8ee61549de5ff5120e945e774728e5ccd57fd3b2ed6eace020ec744823d4a98e242be1453d21764a30a14769ecd62170fba28539b211799bbaf232bbb2789 languageName: node linkType: hard @@ -3482,6 +3489,36 @@ __metadata: languageName: node linkType: hard +"@bull-board/api@npm:4.2.2": + version: 4.2.2 + resolution: "@bull-board/api@npm:4.2.2" + dependencies: + redis-info: ^3.0.8 + checksum: 547174f63d611a568303ad261d3f41a57f632ea2067e8ab900bad90dbc9790a45af8229765350ef47a2eba4300656e8cc792bdcef94003a531f4eabeb4982876 + languageName: node + linkType: hard + +"@bull-board/express@npm:^4.2.2": + version: 4.2.2 + resolution: "@bull-board/express@npm:4.2.2" + dependencies: + "@bull-board/api": 4.2.2 + "@bull-board/ui": 4.2.2 + ejs: 3.1.7 + express: 4.17.3 + checksum: 48d0a5547365cda43186ba5b8019afc573406c105fed951b51893d5a3d64c00eee6c9bd6403eb97b05b3def8b01f57bf53bdceadb33897d05244e43726b21223 + languageName: node + linkType: hard + +"@bull-board/ui@npm:4.2.2": + version: 4.2.2 + resolution: "@bull-board/ui@npm:4.2.2" + dependencies: + "@bull-board/api": 4.2.2 + checksum: b7c5ecc35231fd6bd0dc38b39bc272b646b6336b99b8bfb5bfef0b9cf22bdc129eaeefb23116a17afd611d4e51a867601fd4eb5f6034530540e316e986bd5002 + languageName: node + linkType: hard + "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -4725,6 +4762,13 @@ __metadata: languageName: node linkType: hard +"@ioredis/commands@npm:^1.1.1": + version: 1.2.0 + resolution: "@ioredis/commands@npm:1.2.0" + checksum: 9b20225ba36ef3e5caf69b3c0720597c3016cc9b1e157f519ea388f621dd9037177f84cfe7e25c4c32dad7dd90c70ff9123cd411f747e053cf292193c9c461e2 + languageName: node + linkType: hard + "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -4856,6 +4900,48 @@ __metadata: languageName: node linkType: hard +"@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:2.1.2": + version: 2.1.2 + resolution: "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:2.1.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-darwin-x64@npm:2.1.2": + version: 2.1.2 + resolution: "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:2.1.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-arm64@npm:2.1.2": + version: 2.1.2 + resolution: "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:2.1.2" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-arm@npm:2.1.2": + version: 2.1.2 + resolution: "@msgpackr-extract/msgpackr-extract-linux-arm@npm:2.1.2" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-x64@npm:2.1.2": + version: 2.1.2 + resolution: "@msgpackr-extract/msgpackr-extract-linux-x64@npm:2.1.2" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-win32-x64@npm:2.1.2": + version: 2.1.2 + resolution: "@msgpackr-extract/msgpackr-extract-win32-x64@npm:2.1.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@n1ru4l/graphql-live-query@npm:^0.10.0": version: 0.10.0 resolution: "@n1ru4l/graphql-live-query@npm:0.10.0" @@ -5456,6 +5542,7 @@ __metadata: dependencies: "@aws-sdk/client-s3": ^3.100.0 "@aws-sdk/lib-storage": ^3.100.0 + "@bull-board/express": ^4.2.2 "@faker-js/faker": ^7.1.0 "@godaddy/terminus": ^4.9.0 "@graphql-codegen/cli": 2.11.3 @@ -5465,15 +5552,23 @@ __metadata: "@sentry/node": ^6.17.9 "@sentry/tracing": ^6.17.9 "@swc/core": ^1.2.222 + "@tiptap/core": ^2.0.0-beta.176 + "@types/bull": ^3.15.9 "@types/compression": ^1.7.2 "@types/debug": ^4.1.7 + "@types/deep-equal-in-any-order": ^1.0.1 "@types/ejs": ^3.1.1 "@types/express": ^4.17.13 "@types/lodash": ^4.14.180 "@types/mocha": ^7.0.2 + "@types/mock-require": ^2.0.1 "@types/module-alias": ^2.0.1 + "@types/nodemailer": ^6.4.5 + "@types/sanitize-html": ^2.6.2 + "@types/supertest": ^2.0.12 "@types/verror": ^1.10.6 "@types/yargs": ^17.0.10 + "@types/zxcvbn": ^4.4.1 "@typescript-eslint/eslint-plugin": ^5.32.0 "@typescript-eslint/parser": ^5.32.0 apollo-cache-inmemory: ^1.6.6 @@ -5485,6 +5580,7 @@ __metadata: apollo-server-testing: ^2.19.0 axios: ^0.25.0 bcrypt: ^5.0.0 + bull: ^4.8.5 busboy: ^1.4.0 chai: ^4.2.0 chai-http: ^4.3.0 @@ -5495,12 +5591,14 @@ __metadata: cross-env: ^7.0.3 crypto-random-string: ^3.2.0 dataloader: ^2.0.0 + dayjs: ^1.11.5 debug: ^4.3.1 deep-equal-in-any-order: ^1.1.15 dotenv: ^8.2.0 ejs: ^3.1.8 eslint: ^8.11.0 eslint-config-prettier: ^8.5.0 + eventemitter2: ^6.4.7 express: ^4.17.3 express-async-errors: ^3.1.1 express-session: ^1.17.1 @@ -5511,7 +5609,7 @@ __metadata: graphql-tag: ^2.11.0 graphql-tools: ^4.0.7 http-proxy-middleware: ^1.0.6 - ioredis: ^4.19.4 + ioredis: ^5.2.2 knex: ^2.0.0 lodash: ^4.17.21 mocha: ^7.2.0 @@ -5543,6 +5641,7 @@ __metadata: supertest: ^4.0.2 ts-node: ^10.9.1 tsconfig-paths: ^4.0.0 + type-fest: ^2.19.0 typescript: ^4.6.4 undici: ^5.4.0 verror: ^1.10.1 @@ -6108,6 +6207,16 @@ __metadata: languageName: node linkType: hard +"@types/bull@npm:^3.15.9": + version: 3.15.9 + resolution: "@types/bull@npm:3.15.9" + dependencies: + "@types/ioredis": "*" + "@types/redis": ^2.8.0 + checksum: 060a274e2939ca70f6ddd89842a5a0e46101a5adaba3fcdc6899e7e64382d01513a954dc5dabfa73993ce584e9a74b3c85b30dcd38362cfcf6eee1416bbf7463 + languageName: node + linkType: hard + "@types/chai@npm:4": version: 4.3.1 resolution: "@types/chai@npm:4.3.1" @@ -6185,6 +6294,13 @@ __metadata: languageName: node linkType: hard +"@types/deep-equal-in-any-order@npm:^1.0.1": + version: 1.0.1 + resolution: "@types/deep-equal-in-any-order@npm:1.0.1" + checksum: 09a76203438bc2ed9e3ac3f22b6fd43f3561ac4ae5403cf62b4e69359f3016680fbd6ce31f80704e310c3616f4d134e3252ba70e963fc4ca4dd261719c6eb4fd + languageName: node + linkType: hard + "@types/dompurify@npm:^2.3.3": version: 2.3.3 resolution: "@types/dompurify@npm:2.3.3" @@ -6347,6 +6463,15 @@ __metadata: languageName: node linkType: hard +"@types/ioredis@npm:*": + version: 4.28.10 + resolution: "@types/ioredis@npm:4.28.10" + dependencies: + "@types/node": "*" + checksum: 0f2788cf25f490d3b345db8c5f8b8ce3f6c92cc99abcf744c8f974f02b9b3875233b3d22098614c462a0d6c41c523bd655509418ea88eb6249db6652290ce7cf + languageName: node + linkType: hard + "@types/js-yaml@npm:^4.0.0": version: 4.0.5 resolution: "@types/js-yaml@npm:4.0.5" @@ -6470,6 +6595,15 @@ __metadata: languageName: node linkType: hard +"@types/mock-require@npm:^2.0.1": + version: 2.0.1 + resolution: "@types/mock-require@npm:2.0.1" + dependencies: + "@types/node": "*" + checksum: 8749a4b3fcb9f3d6ebaeff442f00997ca59c4806bc00fea648a1fd06b1ea8510a6900b8e47070561ddf15ce98abc80dfe24ff21a307c2b0d1a6845bd865f708b + languageName: node + linkType: hard + "@types/module-alias@npm:^2.0.1": version: 2.0.1 resolution: "@types/module-alias@npm:2.0.1" @@ -6505,6 +6639,15 @@ __metadata: languageName: node linkType: hard +"@types/nodemailer@npm:^6.4.5": + version: 6.4.5 + resolution: "@types/nodemailer@npm:6.4.5" + dependencies: + "@types/node": "*" + checksum: ecbe34a6eb5559bdca58115b5c03ff048896e14dc7ea6426b3fdfc03484764eb9e4100fcff37b96d012543ca47238c973d0f5bbfb1ab5a2d1f1f25d494714c59 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -6631,6 +6774,15 @@ __metadata: languageName: node linkType: hard +"@types/redis@npm:^2.8.0": + version: 2.8.32 + resolution: "@types/redis@npm:2.8.32" + dependencies: + "@types/node": "*" + checksum: 2b12103e05977941870c9a248f6ea51f4b7ad7e0f16a7403799c2ed1b3e63b60f693c39f9186be0ea02776934c4595ddcd2a5bde41e530aaad42d26449f6a669 + languageName: node + linkType: hard + "@types/resolve@npm:1.17.1": version: 1.17.1 resolution: "@types/resolve@npm:1.17.1" @@ -6656,6 +6808,15 @@ __metadata: languageName: node linkType: hard +"@types/sanitize-html@npm:^2.6.2": + version: 2.6.2 + resolution: "@types/sanitize-html@npm:2.6.2" + dependencies: + htmlparser2: ^6.0.0 + checksum: 08b43427427cbd8acd2843bbf9e00576c06e3916fc523d27fd9016f39563f7999f78b632ff473ef83a77f86bdea9286de2f81e3a8f8a05af6721687651c84f1c + languageName: node + linkType: hard + "@types/serve-index@npm:^1.9.1": version: 1.9.1 resolution: "@types/serve-index@npm:1.9.1" @@ -6684,6 +6845,16 @@ __metadata: languageName: node linkType: hard +"@types/superagent@npm:*": + version: 4.1.15 + resolution: "@types/superagent@npm:4.1.15" + dependencies: + "@types/cookiejar": "*" + "@types/node": "*" + checksum: 347cd74ef0a29e6b9c6d32253c3fb0dd39a31618b50752f84d36b6a9246237bb6b68c9b436c1f94adabc2df89d9f1939e4782f4c850f98b9c2fe431ad4e565a4 + languageName: node + linkType: hard + "@types/superagent@npm:^3.8.3": version: 3.8.7 resolution: "@types/superagent@npm:3.8.7" @@ -6694,6 +6865,15 @@ __metadata: languageName: node linkType: hard +"@types/supertest@npm:^2.0.12": + version: 2.0.12 + resolution: "@types/supertest@npm:2.0.12" + dependencies: + "@types/superagent": "*" + checksum: f0e2b44f86bec2f708d6a3d0cb209055b487922040773049b0f8c6b557af52d4b5fa904e17dfaa4ce6e610172206bbec7b62420d158fa57b6ffc2de37b1730d3 + languageName: node + linkType: hard + "@types/three@npm:^0.136.0": version: 0.136.1 resolution: "@types/three@npm:0.136.1" @@ -6788,6 +6968,13 @@ __metadata: languageName: node linkType: hard +"@types/zxcvbn@npm:^4.4.1": + version: 4.4.1 + resolution: "@types/zxcvbn@npm:4.4.1" + checksum: 71c484f1aa049921997ee6471604725a7fb00b9899e7753c6e94275a2b135e1088ed3f2f2ba77d189715d1ec61bed65b06899ba03e061b07ffc32c2a9296501b + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:^5.0.0": version: 5.28.0 resolution: "@typescript-eslint/eslint-plugin@npm:5.28.0" @@ -9786,6 +9973,24 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:1.19.2": + version: 1.19.2 + resolution: "body-parser@npm:1.19.2" + dependencies: + bytes: 3.1.2 + content-type: ~1.0.4 + debug: 2.6.9 + depd: ~1.1.2 + http-errors: 1.8.1 + iconv-lite: 0.4.24 + on-finished: ~2.3.0 + qs: 6.9.7 + raw-body: 2.4.3 + type-is: ~1.6.18 + checksum: 7f777ea65670e2622ca4a785b5dcb2a68451b3bb8d4d0f41091d307d56b640dba588a9ae04d85dda2cdd5e42788266a783528d5417e5643720fd611fd52522e7 + languageName: node + linkType: hard + "body-parser@npm:1.20.0, body-parser@npm:^1.18.3": version: 1.20.0 resolution: "body-parser@npm:1.20.0" @@ -10177,6 +10382,23 @@ __metadata: languageName: node linkType: hard +"bull@npm:^4.8.5": + version: 4.8.5 + resolution: "bull@npm:4.8.5" + dependencies: + cron-parser: ^4.2.1 + debuglog: ^1.0.0 + get-port: ^5.1.1 + ioredis: ^4.28.5 + lodash: ^4.17.21 + msgpackr: ^1.5.2 + p-timeout: ^3.2.0 + semver: ^7.3.2 + uuid: ^8.3.0 + checksum: bf95a6661db066b693c8fd75f1b73df5ffee57f4dfd8478530b71c0cf678c05536d37480bf4c926aa3e1ccb69336e15cf8e493d5f730f9f3d6de3fe1acd369c5 + languageName: node + linkType: hard + "bunyan@npm:^1.8.14": version: 1.8.15 resolution: "bunyan@npm:1.8.15" @@ -11303,7 +11525,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^8.2.0, commander@npm:^8.3.0": +"commander@npm:^8.3.0": version: 8.3.0 resolution: "commander@npm:8.3.0" checksum: 0f82321821fc27b83bd409510bb9deeebcfa799ff0bf5d102128b500b7af22872c0c92cb6a0ebc5a4cf19c6b550fba9cedfa7329d18c6442a625f851377bacf0 @@ -11590,6 +11812,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:0.4.2, cookie@npm:^0.4.1": + version: 0.4.2 + resolution: "cookie@npm:0.4.2" + checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b + languageName: node + linkType: hard + "cookie@npm:0.5.0": version: 0.5.0 resolution: "cookie@npm:0.5.0" @@ -11597,13 +11826,6 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.4.1": - version: 0.4.2 - resolution: "cookie@npm:0.4.2" - checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b - languageName: node - linkType: hard - "cookiejar@npm:^2.1.0, cookiejar@npm:^2.1.1": version: 2.1.3 resolution: "cookiejar@npm:2.1.3" @@ -11850,6 +12072,15 @@ __metadata: languageName: node linkType: hard +"cron-parser@npm:^4.2.1": + version: 4.6.0 + resolution: "cron-parser@npm:4.6.0" + dependencies: + luxon: ^3.0.1 + checksum: cef63dee396732a8247c2c55d99512db7ad39797459f4bfd534ce5c18efdbf88b16ae8265c3b2abc40cdfadf8930bb1be6778e6ae664ae70e4ed7f206487d0cd + languageName: node + linkType: hard + "cross-env@npm:^7.0.3": version: 7.0.3 resolution: "cross-env@npm:7.0.3" @@ -12429,6 +12660,13 @@ __metadata: languageName: node linkType: hard +"dayjs@npm:^1.11.5": + version: 1.11.5 + resolution: "dayjs@npm:1.11.5" + checksum: e3bbaa7b4883b31be4bf75a181f1447fbb19800c29b332852125aab96baeff3ac232dcba8b88c4ea17d3b636c99dac5fb9d1af4bb6ae26615698bbc4a852dffb + languageName: node + linkType: hard + "debounce@npm:^1.2.0": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -12475,6 +12713,13 @@ __metadata: languageName: node linkType: hard +"debuglog@npm:^1.0.0": + version: 1.0.1 + resolution: "debuglog@npm:1.0.1" + checksum: 970679f2eb7a73867e04d45b52583e7ec6dee1f33c058e9147702e72a665a9647f9c3d6e7c2f66f6bf18510b23eb5ded1b617e48ac1db23603809c5ddbbb9763 + languageName: node + linkType: hard + "decache@npm:^4.6.0": version: 4.6.1 resolution: "decache@npm:4.6.1" @@ -12804,6 +13049,13 @@ __metadata: languageName: node linkType: hard +"denque@npm:^2.0.1": + version: 2.1.0 + resolution: "denque@npm:2.1.0" + checksum: 1d4ae1d05e59ac3a3481e7b478293f4b4c813819342273f3d5b826c7ffa9753c520919ba264f377e09108d24ec6cf0ec0ac729a5686cbb8f32d797126c5dae74 + languageName: node + linkType: hard + "depd@npm:2.0.0, depd@npm:^2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -13258,6 +13510,17 @@ __metadata: languageName: node linkType: hard +"ejs@npm:3.1.7": + version: 3.1.7 + resolution: "ejs@npm:3.1.7" + dependencies: + jake: ^10.8.5 + bin: + ejs: bin/cli.js + checksum: fe40764af39955ce8f8b116716fc8b911959946698edb49ecab85df597746c07aa65d5b74ead28a1e2ffa75b0f92d9bedd752f1c29437da6137b3518271e988c + languageName: node + linkType: hard + "ejs@npm:^2.6.1, ejs@npm:^2.7.1": version: 2.7.4 resolution: "ejs@npm:2.7.4" @@ -14131,6 +14394,13 @@ __metadata: languageName: node linkType: hard +"eventemitter2@npm:^6.4.7": + version: 6.4.7 + resolution: "eventemitter2@npm:6.4.7" + checksum: 1b36a77e139d6965ebf3a36c01fa00c089ae6b80faa1911e52888f40b3a7057b36a2cc45dcd1ad87cda3798fe7b97a0aabcbb8175a8b96092a23bb7d0f039e66 + languageName: node + linkType: hard + "eventemitter3@npm:^2.0.3": version: 2.0.3 resolution: "eventemitter3@npm:2.0.3" @@ -14338,6 +14608,44 @@ __metadata: languageName: node linkType: hard +"express@npm:4.17.3": + version: 4.17.3 + resolution: "express@npm:4.17.3" + dependencies: + accepts: ~1.3.8 + array-flatten: 1.1.1 + body-parser: 1.19.2 + content-disposition: 0.5.4 + content-type: ~1.0.4 + cookie: 0.4.2 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: ~1.1.2 + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + etag: ~1.8.1 + finalhandler: ~1.1.2 + fresh: 0.5.2 + merge-descriptors: 1.0.1 + methods: ~1.1.2 + on-finished: ~2.3.0 + parseurl: ~1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: ~2.0.7 + qs: 6.9.7 + range-parser: ~1.2.1 + safe-buffer: 5.2.1 + send: 0.17.2 + serve-static: 1.14.2 + setprototypeof: 1.2.0 + statuses: ~1.5.0 + type-is: ~1.6.18 + utils-merge: 1.0.1 + vary: ~1.1.2 + checksum: 967e53b74a37eafdf9789b9938c8df86102928b4985b1ad5e385c709deeab405a364de95ca744bc2cc5d05b5d9cc1efc69ae2ae17688a462038648d5a924bfad + languageName: node + linkType: hard + "express@npm:^4.16.3, express@npm:^4.17.1, express@npm:^4.17.3": version: 4.18.1 resolution: "express@npm:4.18.1" @@ -14871,6 +15179,21 @@ __metadata: languageName: node linkType: hard +"finalhandler@npm:~1.1.2": + version: 1.1.2 + resolution: "finalhandler@npm:1.1.2" + dependencies: + debug: 2.6.9 + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + on-finished: ~2.3.0 + parseurl: ~1.3.3 + statuses: ~1.5.0 + unpipe: ~1.0.0 + checksum: 617880460c5138dd7ccfd555cb5dde4d8f170f4b31b8bd51e4b646bb2946c30f7db716428a1f2882d730d2b72afb47d1f67cc487b874cb15426f95753a88965e + languageName: node + linkType: hard + "find-cache-dir@npm:^2.0.0, find-cache-dir@npm:^2.1.0": version: 2.1.0 resolution: "find-cache-dir@npm:2.1.0" @@ -15527,6 +15850,13 @@ __metadata: languageName: node linkType: hard +"get-port@npm:^5.1.1": + version: 5.1.1 + resolution: "get-port@npm:5.1.1" + checksum: 0162663ffe5c09e748cd79d97b74cd70e5a5c84b760a475ce5767b357fb2a57cb821cee412d646aa8a156ed39b78aab88974eddaa9e5ee926173c036c0713787 + languageName: node + linkType: hard + "get-proxy@npm:^2.0.0": version: 2.1.0 resolution: "get-proxy@npm:2.1.0" @@ -16126,23 +16456,7 @@ __metadata: languageName: node linkType: hard -"graphql@npm:14 - 16": - version: 16.5.0 - resolution: "graphql@npm:16.5.0" - checksum: a82a926d085818934d04fdf303a269af170e79de943678bd2726370a96194f9454ade9d6d76c2de69afbd7b9f0b4f8061619baecbbddbe82125860e675ac219e - languageName: node - linkType: hard - -"graphql@npm:^14.6.0": - version: 14.7.0 - resolution: "graphql@npm:14.7.0" - dependencies: - iterall: ^1.2.2 - checksum: e5f4e60799421a573904f390e1ec0aa76360f751688dbbe62e9c35baa0d3727c8d59a659bfc524f126dffe3518da87fd8ecaa78c94fd5c0fe4e035c670745242 - languageName: node - linkType: hard - -"graphql@npm:^15, graphql@npm:^15.0.0": +"graphql@npm:^15": version: 15.8.0 resolution: "graphql@npm:15.8.0" checksum: 423325271db8858428641b9aca01699283d1fe5b40ef6d4ac622569ecca927019fce8196208b91dd1d8eb8114f00263fe661d241d0eb40c10e5bfd650f86ec5e @@ -16665,6 +16979,19 @@ __metadata: languageName: node linkType: hard +"http-errors@npm:1.8.1, http-errors@npm:^1.6.3, http-errors@npm:^1.7.3, http-errors@npm:^1.8.0, http-errors@npm:~1.8.0": + version: 1.8.1 + resolution: "http-errors@npm:1.8.1" + dependencies: + depd: ~1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: ">= 1.5.0 < 2" + toidentifier: 1.0.1 + checksum: d3c7e7e776fd51c0a812baff570bdf06fe49a5dc448b700ab6171b1250e4cf7db8b8f4c0b133e4bfe2451022a5790c1ca6c2cae4094dedd6ac8304a1267f91d2 + languageName: node + linkType: hard + "http-errors@npm:2.0.0": version: 2.0.0 resolution: "http-errors@npm:2.0.0" @@ -16678,19 +17005,6 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:^1.6.3, http-errors@npm:^1.7.3, http-errors@npm:^1.8.0, http-errors@npm:~1.8.0": - version: 1.8.1 - resolution: "http-errors@npm:1.8.1" - dependencies: - depd: ~1.1.2 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: ">= 1.5.0 < 2" - toidentifier: 1.0.1 - checksum: d3c7e7e776fd51c0a812baff570bdf06fe49a5dc448b700ab6171b1250e4cf7db8b8f4c0b133e4bfe2451022a5790c1ca6c2cae4094dedd6ac8304a1267f91d2 - languageName: node - linkType: hard - "http-parser-js@npm:>=0.5.1": version: 0.5.6 resolution: "http-parser-js@npm:0.5.6" @@ -17256,7 +17570,7 @@ __metadata: languageName: node linkType: hard -"ioredis@npm:^4.17.3, ioredis@npm:^4.19.4": +"ioredis@npm:^4.17.3, ioredis@npm:^4.28.5": version: 4.28.5 resolution: "ioredis@npm:4.28.5" dependencies: @@ -17275,6 +17589,23 @@ __metadata: languageName: node linkType: hard +"ioredis@npm:^5.2.2": + version: 5.2.2 + resolution: "ioredis@npm:5.2.2" + dependencies: + "@ioredis/commands": ^1.1.1 + cluster-key-slot: ^1.1.0 + debug: ^4.3.4 + denque: ^2.0.1 + lodash.defaults: ^4.2.0 + lodash.isarguments: ^3.1.0 + redis-errors: ^1.2.0 + redis-parser: ^3.0.0 + standard-as-callback: ^2.1.0 + checksum: 8d650326063de4e37ec7ab38f00dde555284d9fc99fcbe698a4b0d65214920b8cb0f9483c0390fef8d81e9ee26e65380c93113cdedee184206815787bf88cec7 + languageName: node + linkType: hard + "ip-regex@npm:^2.0.0, ip-regex@npm:^2.1.0": version: 2.1.0 resolution: "ip-regex@npm:2.1.0" @@ -18178,7 +18509,7 @@ __metadata: languageName: node linkType: hard -"iterall@npm:^1.1.1, iterall@npm:^1.1.3, iterall@npm:^1.2.1, iterall@npm:^1.2.2, iterall@npm:^1.3.0": +"iterall@npm:^1.1.1, iterall@npm:^1.1.3, iterall@npm:^1.2.1, iterall@npm:^1.3.0": version: 1.3.0 resolution: "iterall@npm:1.3.0" checksum: c78b99678f8c99be488cca7f33e4acca9b72c1326e050afbaf023f086e55619ee466af0464af94a0cb3f292e60cb5bac53a8fd86bd4249ecad26e09f17bb158b @@ -19586,6 +19917,13 @@ __metadata: languageName: node linkType: hard +"luxon@npm:^3.0.1": + version: 3.0.1 + resolution: "luxon@npm:3.0.1" + checksum: aa966eb919bf95b1bd819cda784d1f6f66e3fb65bd9ec7bf68b6a978eeb4e3e14f7e2275021b473f93b15b6b7ba2e5a30471e53add3929a7e695fcfd6dd40ec8 + languageName: node + linkType: hard + "lws-basic-auth@npm:^2.0.0": version: 2.0.0 resolution: "lws-basic-auth@npm:2.0.0" @@ -20499,6 +20837,49 @@ __metadata: languageName: node linkType: hard +"msgpackr-extract@npm:^2.0.2": + version: 2.1.2 + resolution: "msgpackr-extract@npm:2.1.2" + dependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64": 2.1.2 + "@msgpackr-extract/msgpackr-extract-darwin-x64": 2.1.2 + "@msgpackr-extract/msgpackr-extract-linux-arm": 2.1.2 + "@msgpackr-extract/msgpackr-extract-linux-arm64": 2.1.2 + "@msgpackr-extract/msgpackr-extract-linux-x64": 2.1.2 + "@msgpackr-extract/msgpackr-extract-win32-x64": 2.1.2 + node-gyp: latest + node-gyp-build-optional-packages: 5.0.3 + dependenciesMeta: + "@msgpackr-extract/msgpackr-extract-darwin-arm64": + optional: true + "@msgpackr-extract/msgpackr-extract-darwin-x64": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-arm": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-arm64": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-x64": + optional: true + "@msgpackr-extract/msgpackr-extract-win32-x64": + optional: true + bin: + download-msgpackr-prebuilds: bin/download-prebuilds.js + checksum: bf068baa690d3e5c5609c10aa363901ac43d3f32b9d89f9dfb77293afa866eb1b943482338da6c38d50790a66c966fd7e0fbc9187b2a35f40f253931f649f97f + languageName: node + linkType: hard + +"msgpackr@npm:^1.5.2": + version: 1.6.2 + resolution: "msgpackr@npm:1.6.2" + dependencies: + msgpackr-extract: ^2.0.2 + dependenciesMeta: + msgpackr-extract: + optional: true + checksum: 1bb1ac0d1b5de491c835e330769f090608a19d349689f73204979258d22836419f81456a6e911adc301f68b5e06cb28ed289e135efb605e2a0f03a8784b42f62 + languageName: node + linkType: hard + "multicast-dns-service-types@npm:^1.1.0": version: 1.1.0 resolution: "multicast-dns-service-types@npm:1.1.0" @@ -20795,6 +21176,17 @@ __metadata: languageName: node linkType: hard +"node-gyp-build-optional-packages@npm:5.0.3": + version: 5.0.3 + resolution: "node-gyp-build-optional-packages@npm:5.0.3" + bin: + node-gyp-build-optional-packages: bin.js + node-gyp-build-optional-packages-optional: optional.js + node-gyp-build-optional-packages-test: build-test.js + checksum: be3f0235925c8361e5bc1a03848f5e24815b0df8aa90bd13f1eac91cd86264bbb8b7689ca6cd083b02c8099c7b54f9fb83066c7bb77c2389dc4eceab921f084f + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 9.0.0 resolution: "node-gyp@npm:9.0.0" @@ -21668,6 +22060,15 @@ __metadata: languageName: node linkType: hard +"p-timeout@npm:^3.2.0": + version: 3.2.0 + resolution: "p-timeout@npm:3.2.0" + dependencies: + p-finally: ^1.0.0 + checksum: 3dd0eaa048780a6f23e5855df3dd45c7beacff1f820476c1d0d1bcd6648e3298752ba2c877aa1c92f6453c7dd23faaf13d9f5149fc14c0598a142e2c5e8d649c + languageName: node + linkType: hard + "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0" @@ -22970,11 +23371,11 @@ __metadata: linkType: hard "prettier@npm:^2.4.1": - version: 2.7.0 - resolution: "prettier@npm:2.7.0" + version: 2.7.1 + resolution: "prettier@npm:2.7.1" bin: prettier: bin-prettier.js - checksum: 5b55bb1dced9d16635b83229df8e670d150890fdb343f19e8a66e610094a108e960c0f57352b3e5cdbc4eff4ef00a834406047ffcd9f20bd22a6497ba143c81f + checksum: 55a4409182260866ab31284d929b3cb961e5fdb91fe0d2e099dac92eaecec890f36e524b4c19e6ceae839c99c6d7195817579cdffc8e2c80da0cb794463a748b languageName: node linkType: hard @@ -23421,6 +23822,13 @@ __metadata: languageName: node linkType: hard +"qs@npm:6.9.7": + version: 6.9.7 + resolution: "qs@npm:6.9.7" + checksum: 5bbd263332ccf320a1f36d04a2019a5834dc20bcb736431eaccde2a39dcba03fb26d2fd00174f5d7bc26aaad1cad86124b18440883ac042ea2a0fca6170c1bf1 + languageName: node + linkType: hard + "qs@npm:~6.5.2": version: 6.5.3 resolution: "qs@npm:6.5.3" @@ -23529,6 +23937,18 @@ __metadata: languageName: node linkType: hard +"raw-body@npm:2.4.3": + version: 2.4.3 + resolution: "raw-body@npm:2.4.3" + dependencies: + bytes: 3.1.2 + http-errors: 1.8.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + checksum: d2961fa3c71c9c22dc2c3fd60ff377bf36dfed7d7a748f2b25d585934a3e9df565bb9aa5bc2e3a716ea941f4bc2a6ddc795c8b0cf7219fb071029b59b1985394 + languageName: node + linkType: hard + "raw-body@npm:2.5.1, raw-body@npm:^2.3.3": version: 2.5.1 resolution: "raw-body@npm:2.5.1" @@ -23724,6 +24144,15 @@ __metadata: languageName: node linkType: hard +"redis-info@npm:^3.0.8": + version: 3.1.0 + resolution: "redis-info@npm:3.1.0" + dependencies: + lodash: ^4.17.11 + checksum: d72ff0584ebb4a2149cfcfcf9142d9a7f9d0b96ae53fbf431f2738f33f1f42add6505ff73b2d640cab345923a34b217d7c728fa706cc81ad8bd8ad4c48987445 + languageName: node + linkType: hard + "redis-parser@npm:^3.0.0": version: 3.0.0 resolution: "redis-parser@npm:3.0.0" @@ -24713,6 +25142,27 @@ __metadata: languageName: node linkType: hard +"send@npm:0.17.2": + version: 0.17.2 + resolution: "send@npm:0.17.2" + dependencies: + debug: 2.6.9 + depd: ~1.1.2 + destroy: ~1.0.4 + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + etag: ~1.8.1 + fresh: 0.5.2 + http-errors: 1.8.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: ~2.3.0 + range-parser: ~1.2.1 + statuses: ~1.5.0 + checksum: c28f36deb4ccba9b8d6e6a1e472b8e7c40a1f51575bdf8f67303568cc9e71131faa3adc36fdb72611616ccad1584358bbe4c3ebf419e663ecc5de868ad3d3f03 + languageName: node + linkType: hard + "send@npm:0.18.0": version: 0.18.0 resolution: "send@npm:0.18.0" @@ -24805,6 +25255,18 @@ __metadata: languageName: node linkType: hard +"serve-static@npm:1.14.2": + version: 1.14.2 + resolution: "serve-static@npm:1.14.2" + dependencies: + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + parseurl: ~1.3.3 + send: 0.17.2 + checksum: d97f3183b1dfcd8ce9c0e37e18e87fd31147ed6c8ee0b2c3a089d795e44ee851ca5061db01574f806d54f4e4b70bc694d9ca64578653514e04a28cbc97a1de05 + languageName: node + linkType: hard + "serve-static@npm:1.15.0": version: 1.15.0 resolution: "serve-static@npm:1.15.0" @@ -25539,7 +26001,7 @@ __metadata: languageName: node linkType: hard -"statuses@npm:>= 1.4.0 < 2, statuses@npm:>= 1.5.0 < 2, statuses@npm:^1.5.0": +"statuses@npm:>= 1.4.0 < 2, statuses@npm:>= 1.5.0 < 2, statuses@npm:^1.5.0, statuses@npm:~1.5.0": version: 1.5.0 resolution: "statuses@npm:1.5.0" checksum: c469b9519de16a4bb19600205cffb39ee471a5f17b82589757ca7bd40a8d92ebb6ed9f98b5a540c5d302ccbc78f15dc03cc0280dd6e00df1335568a5d5758a5c @@ -27126,6 +27588,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^2.19.0": + version: 2.19.0 + resolution: "type-fest@npm:2.19.0" + checksum: a4ef07ece297c9fba78fc1bd6d85dff4472fe043ede98bd4710d2615d15776902b595abf62bd78339ed6278f021235fb28a96361f8be86ed754f778973a0d278 + languageName: node + linkType: hard + "type-is@npm:^1.6.16, type-is@npm:~1.6.16, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -27825,9 +28294,9 @@ __metadata: languageName: node linkType: hard -"vls@npm:^0.7.6": - version: 0.7.6 - resolution: "vls@npm:0.7.6" +"vls@npm:^0.8.1": + version: 0.8.1 + resolution: "vls@npm:0.8.1" dependencies: eslint: ^7.32.0 eslint-plugin-vue: ^7.19.1 @@ -27837,7 +28306,7 @@ __metadata: typescript: ^4.4.4 bin: vls: bin/vls - checksum: 69a209a45b3a25f71c3e4cf60719caaf16ae21fa550a7136c43690e1a373a88be3cd040df6624bfaa6993bb6139d5bb2be59d92015b7289167f19d05bfd687e6 + checksum: 604751b2363be3616a1db3272dafac7a02572f5efa3339e48b1f89bc92254713cc44227999dedbf3c04d6d9f52a2268dd834fcb57b51dff1db215839efe404be languageName: node linkType: hard @@ -27849,14 +28318,14 @@ __metadata: linkType: hard "vti@npm:^0.1.5": - version: 0.1.5 - resolution: "vti@npm:0.1.5" + version: 0.1.7 + resolution: "vti@npm:0.1.7" dependencies: - commander: ^8.2.0 - vls: ^0.7.6 + commander: ^8.3.0 + vls: ^0.8.1 bin: vti: bin/vti - checksum: c9fb2b52ba7b85ce57cb5375d5656ea5414a82e2305a621c7e54f6489f61bfb52877893e4ae3f2be6b61bf8cca5b84539c6db4f8306bfc79d416929c63a2e4e0 + checksum: 9e7189bb89c75beff58aacaa03050bc2e6e38db657983a19b5b94fd9d4d372bdd57d98c0072df684accdaec7067316dc7b5ab138ea56f0945b56a3d77d6b2638 languageName: node linkType: hard