From 6a0ddb5d985f3b1c4bec8c1d7a02903f16485247 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:56:07 +0100 Subject: [PATCH 01/19] chore(release): Test v7.17.0 (#9448) * chore(env vars): Stripe vars moved to the Integrations section (#9427) * chore: fix misleading `isLead` field name on `Team` (#9413) * chore: fix misleading `isLead` field name on `Team` The field indicates whether the viewer is the lead, but when used in a query for a different user, the result could be read wrong. * Fix Team.isLead dependencies * feat: remove team template limit (#9424) * update error message and increase template limit * remove max team template limits * remove canClone prop from CloneTemplate * remove unused threshold * remove unused threshold * feat: Add Google calendar meeting series for recurrence (#9380) * feat: Add recurrence to GCal events * Fun with timezones * fix: Increase the number of projects fetched per request from Atlassian (#9435) We ran into timeouts in `getAllProjects`, presumably because we're doing too many roundtrips. As a quick fix, increse the number of projects fetched per request from 50 to 500. * chore(deps): bump ip from 1.1.8 to 1.1.9 (#9442) Bumps [ip](https://github.com/indutny/node-ip) from 1.1.8 to 1.1.9. - [Commits](https://github.com/indutny/node-ip/compare/v1.1.8...v1.1.9) --- updated-dependencies: - dependency-name: ip dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(release): release v7.17.0 (#9428) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: Rafa <101704572+rafaelromcar-parabol@users.noreply.github.com> Co-authored-by: Georg Bremer Co-authored-by: Nick O'Ferrall Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> --- .env.example | 6 +- .release-please-manifest.json | 2 +- CHANGELOG.md | 20 +++++ package.json | 2 +- packages/chronos/package.json | 4 +- .../ActivityDetails/TemplateDetails.tsx | 2 +- .../CreateNewActivity/CreateNewActivity.tsx | 34 +------- .../ReviewRequestToJoinOrgModal.tsx | 2 +- .../components/AddNewPokerTemplate.tsx | 14 +--- .../components/AddNewReflectTemplate.tsx | 11 +-- .../meeting/components/CloneTemplate.tsx | 9 +-- .../components/PokerTemplateDetails.tsx | 7 +- .../components/ReflectTemplateDetails.tsx | 7 +- .../components/ManageTeam/ManageTeamList.tsx | 4 +- .../components/TeamSettings/TeamSettings.tsx | 2 +- .../mutations/PromoteToTeamLeadMutation.ts | 2 +- packages/client/package.json | 2 +- packages/client/types/constEnums.ts | 2 - packages/gql-executor/package.json | 6 +- packages/integration-tests/package.json | 2 +- .../graphql/mutations/addPokerTemplate.ts | 5 +- .../graphql/mutations/addReflectTemplate.ts | 5 +- .../mutations/helpers/createGcalEvent.ts | 80 ++++++++++++++++--- .../public/mutations/startRetrospective.ts | 18 ++++- .../mutations/updateRecurrenceSettings.ts | 26 ++++++ .../graphql/public/typeDefs/Team.graphql | 2 +- packages/server/graphql/types/Team.ts | 2 +- packages/server/package.json | 5 +- ...06021181176_addGCalEventToMeetingSeries.ts | 22 +++++ .../server/utils/AtlassianServerManager.ts | 3 +- yarn.lock | 20 ++--- 31 files changed, 203 insertions(+), 125 deletions(-) create mode 100644 packages/server/postgres/migrations/1706021181176_addGCalEventToMeetingSeries.ts diff --git a/.env.example b/.env.example index be16a7edd47..88cdde703de 100644 --- a/.env.example +++ b/.env.example @@ -112,6 +112,9 @@ RETHINKDB_SSL='false' # RECALL_AI_KEY='' # SLACK_CLIENT_ID='key_SLACK_CLIENT_ID' # SLACK_CLIENT_SECRET='key_SLACK_CLIENT_SECRET' +# STRIPE_SECRET_KEY='sk_test_4eC39HqLyjWDarjtT1zdp7dc' +# STRIPE_PUBLISHABLE_KEY='pk_test_TYooMQauvdEDq54NiTphI7jx' +# STRIPE_WEBHOOK_SECRET='sk_test_4eC39HqLyjWDarjtT1zdp7dc' # MAIL # MAIL GLOBALS. PROVIDER: mailgun | google | debug | smtp @@ -127,9 +130,6 @@ MAIL_PROVIDER='debug' # MAIL_SMTP_PASSWORD='key_MAIL_SMTP_PASSWORD' # MAIL_SMTP_USE_TLS='1' # set to '0' for false # MAIL_SMTP_CIPHERS='HIGH:MEDIUM:!aNULL:!eNULL:@STRENGTH:!DH:!kEDH' -# STRIPE_SECRET_KEY='sk_test_4eC39HqLyjWDarjtT1zdp7dc' -# STRIPE_PUBLISHABLE_KEY='pk_test_TYooMQauvdEDq54NiTphI7jx' -# STRIPE_WEBHOOK_SECRET='sk_test_4eC39HqLyjWDarjtT1zdp7dc' # DEVELOPER VARIABLES # CI='true' diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2dcd7403b8b..0fb11cbd996 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.16.0" + ".": "7.17.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 3de62f10fbe..fd68e143309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.17.0](https://github.com/ParabolInc/parabol/compare/v7.16.0...v7.17.0) (2024-02-21) + + +### Added + +* Add Google calendar meeting series for recurrence ([#9380](https://github.com/ParabolInc/parabol/issues/9380)) ([02dc6fa](https://github.com/ParabolInc/parabol/commit/02dc6fa6e4687021bb46a6774eb5f0be859e4d3f)) +* remove team template limit ([#9424](https://github.com/ParabolInc/parabol/issues/9424)) ([f042628](https://github.com/ParabolInc/parabol/commit/f042628fef5bbdbf566c49bab729f5b9dec058f1)) + + +### Fixed + +* Increase the number of projects fetched per request from Atlassian ([#9435](https://github.com/ParabolInc/parabol/issues/9435)) ([b0b76f9](https://github.com/ParabolInc/parabol/commit/b0b76f9f45789f60b55243f78eba7b656c751658)) + + +### Changed + +* **deps:** bump ip from 1.1.8 to 1.1.9 ([#9442](https://github.com/ParabolInc/parabol/issues/9442)) ([c2a31e6](https://github.com/ParabolInc/parabol/commit/c2a31e6b8ef2c4f4d375323f8afbef6874024593)) +* **env vars:** Stripe vars moved to the Integrations section ([#9427](https://github.com/ParabolInc/parabol/issues/9427)) ([a0af0c1](https://github.com/ParabolInc/parabol/commit/a0af0c1230a1dbc93a28977d6d61180319220c88)) +* fix misleading `isLead` field name on `Team` ([#9413](https://github.com/ParabolInc/parabol/issues/9413)) ([c0a2fdf](https://github.com/ParabolInc/parabol/commit/c0a2fdf8fb3deaa34f7935ae8a87d30f43381ecd)) + ## [7.16.0](https://github.com/ParabolInc/parabol/compare/v7.15.2...v7.16.0) (2024-02-14) diff --git a/package.json b/package.json index cc585b841d9..edde1656431 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.16.0", + "version": "7.17.0", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/chronos/package.json b/packages/chronos/package.json index 1bc7dbf47ef..5295ced9dd0 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.16.0", + "version": "7.17.0", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -25,6 +25,6 @@ }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.16.0" + "parabol-server": "7.17.0" } } diff --git a/packages/client/components/ActivityLibrary/ActivityDetails/TemplateDetails.tsx b/packages/client/components/ActivityLibrary/ActivityDetails/TemplateDetails.tsx index 0c2b77bef15..b86554fb3ef 100644 --- a/packages/client/components/ActivityLibrary/ActivityDetails/TemplateDetails.tsx +++ b/packages/client/components/ActivityLibrary/ActivityDetails/TemplateDetails.tsx @@ -267,7 +267,7 @@ export const TemplateDetails = (props: Props) => { />
- +
)} diff --git a/packages/client/components/ActivityLibrary/CreateNewActivity/CreateNewActivity.tsx b/packages/client/components/ActivityLibrary/CreateNewActivity/CreateNewActivity.tsx index 2967f5ab866..8346b47a252 100644 --- a/packages/client/components/ActivityLibrary/CreateNewActivity/CreateNewActivity.tsx +++ b/packages/client/components/ActivityLibrary/CreateNewActivity/CreateNewActivity.tsx @@ -4,14 +4,11 @@ import graphql from 'babel-plugin-relay/macro' import * as RadioGroup from '@radix-ui/react-radio-group' import clsx from 'clsx' import {Link} from 'react-router-dom' - import newTemplate from '../../../../../static/images/illustrations/newTemplate.png' import estimatedEffortTemplate from '../../../../../static/images/illustrations/estimatedEffortTemplate.png' - import {CreateNewActivityQuery} from '~/__generated__/CreateNewActivityQuery.graphql' import {ActivityCard, ActivityCardImage} from '../ActivityCard' import {ActivityBadge} from '../ActivityBadge' - import IconLabel from '../../IconLabel' import NewMeetingTeamPicker from '../../NewMeetingTeamPicker' import sortByTier from '../../../utils/sortByTier' @@ -20,7 +17,6 @@ import {AddReflectTemplateMutation$data} from '../../../__generated__/AddReflect import useAtmosphere from '../../../hooks/useAtmosphere' import useMutationProps from '../../../hooks/useMutationProps' import AddReflectTemplateMutation from '../../../mutations/AddReflectTemplateMutation' -import {Threshold} from '../../../types/constEnums' import useRouter from '../../../hooks/useRouter' import {CATEGORY_ID_TO_NAME, CATEGORY_THEMES, CategoryID, DEFAULT_CARD_THEME} from '../Categories' import BaseButton from '../../BaseButton' @@ -111,15 +107,6 @@ const query = graphql` ...NewMeetingTeamPicker_selectedTeam ...NewMeetingTeamPicker_teams } - availableTemplates(first: 2000) @connection(key: "ActivityLibrary_availableTemplates") { - edges { - node { - name - teamId - type - } - } - } } } ` @@ -148,7 +135,7 @@ export const CreateNewActivity = (props: Props) => { return selectedActivity }) const {viewer} = data - const {teams, availableTemplates, preferredTeamId, featureFlags} = viewer + const {teams, preferredTeamId, featureFlags} = viewer const [selectedTeam, setSelectedTeam] = useState( teams.find((team) => team.id === preferredTeamId) ?? sortByTier(teams)[0]! ) @@ -160,16 +147,6 @@ export const CreateNewActivity = (props: Props) => { return } - const teamTemplates = availableTemplates.edges.filter( - (template) => - template.node.teamId === selectedTeam.id && template.node.type === 'retrospective' - ) - - if (teamTemplates.length >= Threshold.MAX_RETRO_TEAM_TEMPLATES) { - onError(new Error('You may only have 20 templates per team. Please remove one first.')) - return - } - submitMutation() AddReflectTemplateMutation( atmosphere, @@ -195,15 +172,6 @@ export const CreateNewActivity = (props: Props) => { return } - const teamTemplates = availableTemplates.edges.filter( - (template) => template.node.teamId === selectedTeam.id && template.node.type === 'poker' - ) - - if (teamTemplates.length >= Threshold.MAX_POKER_TEAM_TEMPLATES) { - onError(new Error('You may only have 20 templates per team. Please remove one first.')) - return - } - submitMutation() AddPokerTemplateMutation( atmosphere, diff --git a/packages/client/components/ReviewRequestToJoinOrgModal.tsx b/packages/client/components/ReviewRequestToJoinOrgModal.tsx index 25c2bde8de0..2604455e6f6 100644 --- a/packages/client/components/ReviewRequestToJoinOrgModal.tsx +++ b/packages/client/components/ReviewRequestToJoinOrgModal.tsx @@ -21,7 +21,7 @@ const ReviewRequestToJoinOrgModalViewerFragment = graphql` teams { id name - isLead + isViewerLead teamMembers(sortBy: "preferredName") { userId } diff --git a/packages/client/modules/meeting/components/AddNewPokerTemplate.tsx b/packages/client/modules/meeting/components/AddNewPokerTemplate.tsx index 766c85dccee..053684315a3 100644 --- a/packages/client/modules/meeting/components/AddNewPokerTemplate.tsx +++ b/packages/client/modules/meeting/components/AddNewPokerTemplate.tsx @@ -7,7 +7,6 @@ import TooltipStyled from '../../../components/TooltipStyled' import useAtmosphere from '../../../hooks/useAtmosphere' import useMutationProps from '../../../hooks/useMutationProps' import AddPokerTemplateMutation from '../../../mutations/AddPokerTemplateMutation' -import {Threshold} from '../../../types/constEnums' import {AddNewPokerTemplate_pokerTemplates$key} from '../../../__generated__/AddNewPokerTemplate_pokerTemplates.graphql' import {AddNewPokerTemplate_team$key} from '../../../__generated__/AddNewPokerTemplate_team.graphql' @@ -79,17 +78,6 @@ const AddNewPokerTemplate = (props: Props) => { displayUpgradeDetails() return } - if (pokerTemplates.length >= Threshold.MAX_RETRO_TEAM_TEMPLATES) { - onError( - new Error( - `You may only have ${Threshold.MAX_RETRO_TEAM_TEMPLATES} templates per team. Please remove one first.` - ) - ) - errorTimerId.current = window.setTimeout(() => { - onCompleted() - }, 8000) - return - } if (pokerTemplates.find((template) => template.name.startsWith('*New Template'))) { onError(new Error('You already have a new template. Try renaming that one first.')) errorTimerId.current = window.setTimeout(() => { @@ -106,7 +94,7 @@ const AddNewPokerTemplate = (props: Props) => { template.name.startsWith('*New Template') ) - if (pokerTemplates.length > Threshold.MAX_POKER_TEAM_TEMPLATES || containsNewTemplate) return null + if (containsNewTemplate) return null return (
{error && {error.message}} diff --git a/packages/client/modules/meeting/components/AddNewReflectTemplate.tsx b/packages/client/modules/meeting/components/AddNewReflectTemplate.tsx index eea0c89ab03..61429332903 100644 --- a/packages/client/modules/meeting/components/AddNewReflectTemplate.tsx +++ b/packages/client/modules/meeting/components/AddNewReflectTemplate.tsx @@ -7,7 +7,6 @@ import TooltipStyled from '../../../components/TooltipStyled' import useAtmosphere from '../../../hooks/useAtmosphere' import useMutationProps from '../../../hooks/useMutationProps' import AddReflectTemplateMutation from '../../../mutations/AddReflectTemplateMutation' -import {Threshold} from '../../../types/constEnums' import {AddNewReflectTemplate_reflectTemplates$key} from '../../../__generated__/AddNewReflectTemplate_reflectTemplates.graphql' import {AddNewReflectTemplate_team$key} from '../../../__generated__/AddNewReflectTemplate_team.graphql' @@ -79,13 +78,6 @@ const AddNewReflectTemplate = (props: Props) => { displayUpgradeDetails() return } - if (reflectTemplates.length >= Threshold.MAX_RETRO_TEAM_TEMPLATES) { - onError(new Error('You may only have 20 templates per team. Please remove one first.')) - errorTimerId.current = window.setTimeout(() => { - onCompleted() - }, 8000) - return - } if (reflectTemplates.find((template) => template.name.startsWith('*New Template'))) { onError(new Error('You already have a new template. Try renaming that one first.')) errorTimerId.current = window.setTimeout(() => { @@ -102,8 +94,7 @@ const AddNewReflectTemplate = (props: Props) => { template.name.startsWith('*New Template') ) - if (reflectTemplates.length > Threshold.MAX_RETRO_TEAM_TEMPLATES || containsNewTemplate) - return null + if (containsNewTemplate) return null return (
{error && {error.message}} diff --git a/packages/client/modules/meeting/components/CloneTemplate.tsx b/packages/client/modules/meeting/components/CloneTemplate.tsx index 66cc6cc7d3e..6fbc5a1460a 100644 --- a/packages/client/modules/meeting/components/CloneTemplate.tsx +++ b/packages/client/modules/meeting/components/CloneTemplate.tsx @@ -2,15 +2,12 @@ import React from 'react' import DetailAction from '../../../components/DetailAction' interface Props { - canClone: boolean onClick: () => void } const CloneTemplate = (props: Props) => { - const {canClone, onClick} = props - const tooltip = canClone ? 'Clone & Edit Template' : 'Too many team templates! Remove one first' - return ( - - ) + const {onClick} = props + const tooltip = 'Clone & Edit Template' + return } export default CloneTemplate diff --git a/packages/client/modules/meeting/components/PokerTemplateDetails.tsx b/packages/client/modules/meeting/components/PokerTemplateDetails.tsx index 3dcb51ec48a..c42794d1508 100644 --- a/packages/client/modules/meeting/components/PokerTemplateDetails.tsx +++ b/packages/client/modules/meeting/components/PokerTemplateDetails.tsx @@ -6,7 +6,6 @@ import useAtmosphere from '../../../hooks/useAtmosphere' import useMutationProps from '../../../hooks/useMutationProps' import AddPokerTemplateMutation from '../../../mutations/AddPokerTemplateMutation' import {PALETTE} from '../../../styles/paletteV3' -import {Threshold} from '../../../types/constEnums' import getTemplateList from '../../../utils/getTemplateList' import useTemplateDescription from '../../../utils/useTemplateDescription' import {PokerTemplateDetails_settings$key} from '../../../__generated__/PokerTemplateDetails_settings.graphql' @@ -108,12 +107,10 @@ const PokerTemplateDetails = (props: Props) => { const lowestScope = getTemplateList(teamId, orgId, activeTemplate) const isOwner = activeTemplate.teamId === teamId const description = useTemplateDescription(lowestScope, activeTemplate, tier) - const templateCount = teamTemplates.length const atmosphere = useAtmosphere() const {onError, onCompleted, submitting, submitMutation} = useMutationProps() - const canClone = templateCount < Threshold.MAX_POKER_TEAM_TEMPLATES const onClone = () => { - if (submitting || !canClone) return + if (submitting) return submitMutation() AddPokerTemplateMutation( atmosphere, @@ -145,7 +142,7 @@ const PokerTemplateDetails = (props: Props) => { type='poker' /> )} - {showClone && } + {showClone && } {description} diff --git a/packages/client/modules/meeting/components/ReflectTemplateDetails.tsx b/packages/client/modules/meeting/components/ReflectTemplateDetails.tsx index dec65509954..2d45402d52e 100644 --- a/packages/client/modules/meeting/components/ReflectTemplateDetails.tsx +++ b/packages/client/modules/meeting/components/ReflectTemplateDetails.tsx @@ -6,7 +6,6 @@ import useAtmosphere from '../../../hooks/useAtmosphere' import useMutationProps from '../../../hooks/useMutationProps' import AddReflectTemplateMutation from '../../../mutations/AddReflectTemplateMutation' import {PALETTE} from '../../../styles/paletteV3' -import {Threshold} from '../../../types/constEnums' import getTemplateList from '../../../utils/getTemplateList' import useTemplateDescription from '../../../utils/useTemplateDescription' import {ReflectTemplateDetails_settings$key} from '../../../__generated__/ReflectTemplateDetails_settings.graphql' @@ -116,12 +115,10 @@ const ReflectTemplateDetails = (props: Props) => { const lowestScope = getTemplateList(teamId, orgId, activeTemplate) const isOwner = activeTemplate.teamId === teamId const description = useTemplateDescription(lowestScope, activeTemplate, tier) - const templateCount = teamTemplates.length const atmosphere = useAtmosphere() const {onError, onCompleted, submitting, submitMutation} = useMutationProps() - const canClone = templateCount < Threshold.MAX_RETRO_TEAM_TEMPLATES const onClone = () => { - if (submitting || !canClone) return + if (submitting) return submitMutation() AddReflectTemplateMutation( atmosphere, @@ -153,7 +150,7 @@ const ReflectTemplateDetails = (props: Props) => { type='retrospective' /> )} - {showClone && } + {showClone && } {description} diff --git a/packages/client/modules/teamDashboard/components/ManageTeam/ManageTeamList.tsx b/packages/client/modules/teamDashboard/components/ManageTeam/ManageTeamList.tsx index a8e8933820e..7ed7de48036 100644 --- a/packages/client/modules/teamDashboard/components/ManageTeam/ManageTeamList.tsx +++ b/packages/client/modules/teamDashboard/components/ManageTeam/ManageTeamList.tsx @@ -25,7 +25,7 @@ const ManageTeamList = (props: Props) => { const team = useFragment( graphql` fragment ManageTeamList_team on Team { - isLead + isViewerLead isOrgAdmin teamMembers(sortBy: "preferredName") { id @@ -36,7 +36,7 @@ const ManageTeamList = (props: Props) => { `, props.team ) - const {isLead: isViewerLead, isOrgAdmin: isViewerOrgAdmin, teamMembers} = team + const {isViewerLead, isOrgAdmin: isViewerOrgAdmin, teamMembers} = team return ( {teamMembers.map((teamMember) => { diff --git a/packages/client/modules/teamDashboard/components/TeamSettings/TeamSettings.tsx b/packages/client/modules/teamDashboard/components/TeamSettings/TeamSettings.tsx index 829308b0006..62ffb8f934f 100644 --- a/packages/client/modules/teamDashboard/components/TeamSettings/TeamSettings.tsx +++ b/packages/client/modules/teamDashboard/components/TeamSettings/TeamSettings.tsx @@ -43,7 +43,7 @@ const query = graphql` viewer { team(teamId: $teamId) { ...ArchiveTeam_team - isLead + isViewerLead id name tier diff --git a/packages/client/mutations/PromoteToTeamLeadMutation.ts b/packages/client/mutations/PromoteToTeamLeadMutation.ts index c47310341c2..af3325b1808 100644 --- a/packages/client/mutations/PromoteToTeamLeadMutation.ts +++ b/packages/client/mutations/PromoteToTeamLeadMutation.ts @@ -5,7 +5,7 @@ import {PromoteToTeamLeadMutation as TPromoteToTeamLeadMutation} from '../__gene graphql` fragment PromoteToTeamLeadMutation_team on PromoteToTeamLeadPayload { team { - isLead + isViewerLead } oldLeader { ...DashboardAvatar_teamMember diff --git a/packages/client/package.json b/packages/client/package.json index 2f4c9cec77d..9faef2216e4 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.16.0", + "version": "7.17.0", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/client/types/constEnums.ts b/packages/client/types/constEnums.ts index fe42a13d6be..28bbd744b85 100644 --- a/packages/client/types/constEnums.ts +++ b/packages/client/types/constEnums.ts @@ -397,8 +397,6 @@ export const enum Threshold { MAX_POKER_TEMPLATE_SCALES = 12, MAX_POKER_SCALE_VALUES = 30, POKER_SCALE_VALUE_MAX_LENGTH = 3, - MAX_RETRO_TEAM_TEMPLATES = 20, - MAX_POKER_TEAM_TEMPLATES = 20, MAX_POKER_DIMENSION_NAME = 50, MAX_QUAL_AI_MEETINGS = 3, MAX_REACTJIS = 12, diff --git a/packages/gql-executor/package.json b/packages/gql-executor/package.json index 519e5f91834..1675bcaf5ea 100644 --- a/packages/gql-executor/package.json +++ b/packages/gql-executor/package.json @@ -1,6 +1,6 @@ { "name": "gql-executor", - "version": "7.16.0", + "version": "7.17.0", "description": "A Stateless GraphQL Executor", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/gqlExecutor#readme", @@ -27,8 +27,8 @@ }, "dependencies": { "dd-trace": "^4.2.0", - "parabol-client": "7.16.0", - "parabol-server": "7.16.0", + "parabol-client": "7.17.0", + "parabol-server": "7.17.0", "undici": "^5.26.2" } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 90e1da55dfc..0190f526ea8 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -2,7 +2,7 @@ "name": "integration-tests", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.16.0", + "version": "7.17.0", "description": "", "main": "index.js", "scripts": { diff --git a/packages/server/graphql/mutations/addPokerTemplate.ts b/packages/server/graphql/mutations/addPokerTemplate.ts index ec470279865..3b4e6dbccc0 100644 --- a/packages/server/graphql/mutations/addPokerTemplate.ts +++ b/packages/server/graphql/mutations/addPokerTemplate.ts @@ -1,5 +1,5 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SprintPokerDefaults, SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums' +import {SprintPokerDefaults, SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../database/rethinkDriver' import PokerTemplate from '../../database/types/PokerTemplate' import TemplateDimension from '../../database/types/TemplateDimension' @@ -45,9 +45,6 @@ const addPokerTemplate = { dataLoader.get('teams').load(teamId), dataLoader.get('users').loadNonNull(viewerId) ]) - if (allTemplates.length >= Threshold.MAX_RETRO_TEAM_TEMPLATES) { - return standardError(new Error('Too many templates'), {userId: viewerId}) - } if (!viewerTeam) { return standardError(new Error('Team not found'), {userId: viewerId}) diff --git a/packages/server/graphql/mutations/addReflectTemplate.ts b/packages/server/graphql/mutations/addReflectTemplate.ts index 46c91713490..d4c3825338a 100644 --- a/packages/server/graphql/mutations/addReflectTemplate.ts +++ b/packages/server/graphql/mutations/addReflectTemplate.ts @@ -1,5 +1,5 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums' +import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {PALETTE} from '../../../client/styles/paletteV3' import getRethink from '../../database/rethinkDriver' import ReflectTemplate from '../../database/types/ReflectTemplate' @@ -47,9 +47,6 @@ const addReflectTemplate = { dataLoader.get('users').loadNonNull(viewerId) ]) - if (allTemplates.length >= Threshold.MAX_RETRO_TEAM_TEMPLATES) { - return standardError(new Error('Too many templates'), {userId: viewerId}) - } if (!viewerTeam) { return standardError(new Error('Team not found'), {userId: viewerId}) } diff --git a/packages/server/graphql/mutations/helpers/createGcalEvent.ts b/packages/server/graphql/mutations/helpers/createGcalEvent.ts index 6e936a13cab..4382ca7dedd 100644 --- a/packages/server/graphql/mutations/helpers/createGcalEvent.ts +++ b/packages/server/graphql/mutations/helpers/createGcalEvent.ts @@ -3,23 +3,41 @@ import makeAppURL from 'parabol-client/utils/makeAppURL' import appOrigin from '../../../appOrigin' import {DataLoaderWorker} from '../../graphql' import standardError from '../../../utils/standardError' -import {CreateGcalEventInput} from '../../public/resolverTypes' +import {CreateGcalEventInput, StandardMutationError} from '../../public/resolverTypes' +import {RRule} from 'rrule' +import {pick} from 'lodash' const emailRemindMinsBeforeMeeting = 24 * 60 const popupRemindMinsBeforeMeeting = 10 +const convertRruleToGcal = (rrule: RRule | null | undefined) => { + if (!rrule) { + return [] + } + + // Google does not allow for all fields in rrule. For example DTSTART and DTEND are not allowed. + // It also has trouble with BYHOUR, BYMINUTE, and BYSECOND. It's best to stick to fields known to work. + // Also strip TZID as google wants the UNTIL field in Z, but rrule only uses that if no TZID is present. + const options = pick(rrule.options, 'freq', 'interval', 'byweekday', 'until', 'count') + const gcalRule = new RRule(options) + return [gcalRule.toString()] +} + type Input = { gcalInput?: CreateGcalEventInput | null meetingId: string viewerId: string teamId: string + rrule?: RRule | null dataLoader: DataLoaderWorker } -const createGcalEvent = async (input: Input) => { - const {gcalInput, meetingId, viewerId, dataLoader, teamId} = input +const createGcalEvent = async ( + input: Input +): Promise<{gcalSeriesId?: string; error?: StandardMutationError}> => { + const {gcalInput, meetingId, viewerId, dataLoader, teamId, rrule} = input if (!gcalInput) { - return {error: null} + return {} } const {startTimestamp, endTimestamp, title, timeZone, invitees, videoType} = gcalInput @@ -57,8 +75,9 @@ const createGcalEvent = async (input: Input) => { } } : undefined + const recurrence = convertRruleToGcal(rrule) - const event = { + const eventInput = { summary: title, description, start: { @@ -69,6 +88,7 @@ const createGcalEvent = async (input: Input) => { dateTime: endDateTime, timeZone }, + recurrence, attendees: attendeesWithEmailObjects, reminders: { useDefault: false, @@ -81,17 +101,59 @@ const createGcalEvent = async (input: Input) => { } try { - await calendar.events.insert({ + const event = await calendar.events.insert({ calendarId: 'primary', - requestBody: event, + requestBody: eventInput, conferenceDataVersion: 1 }) + return {gcalSeriesId: event.data.id ?? undefined} } catch (err) { const error = err instanceof Error ? err : new Error('Unable to create event in gcal') return standardError(error, {userId: viewerId}) } - return { - error: null +} + +export type UpdateGcalSeriesInput = { + gcalSeriesId: string + title?: string + rrule: RRule | null + userId: string + teamId: string + dataLoader: DataLoaderWorker +} +export const updateGcalSeries = async (input: UpdateGcalSeriesInput) => { + const {gcalSeriesId, title, rrule, userId, teamId, dataLoader} = input + + const gcalAuth = await dataLoader.get('freshGcalAuth').load({teamId, userId}) + if (!gcalAuth) { + return standardError(new Error('Could not retrieve Google Calendar auth'), {userId}) + } + const {accessToken: access_token, refreshToken: refresh_token, expiresAt} = gcalAuth + const CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID + const CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET + const REDIRECT_URI = appOrigin + + const expiry_date = expiresAt ? expiresAt.getTime() : undefined + + const oauth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI) + oauth2Client.setCredentials({access_token, refresh_token, expiry_date}) + const calendar = google.calendar({version: 'v3', auth: oauth2Client}) + const recurrence = convertRruleToGcal(rrule) + + try { + const event = await calendar.events.patch({ + calendarId: 'primary', + eventId: gcalSeriesId, + requestBody: { + recurrence, + summary: title + }, + conferenceDataVersion: 1 + }) + return {gcalSeriesId: event.data.id ?? undefined} + } catch (err) { + const error = err instanceof Error ? err : new Error('Unable to create event in gcal') + return standardError(error, {userId}) } } diff --git a/packages/server/graphql/public/mutations/startRetrospective.ts b/packages/server/graphql/public/mutations/startRetrospective.ts index 29583f3d755..266664368c1 100644 --- a/packages/server/graphql/public/mutations/startRetrospective.ts +++ b/packages/server/graphql/public/mutations/startRetrospective.ts @@ -16,6 +16,7 @@ import {IntegrationNotifier} from '../../mutations/helpers/notifications/Integra import {startNewMeetingSeries} from './updateRecurrenceSettings' import safeCreateRetrospective from '../../mutations/helpers/safeCreateRetrospective' import {createMeetingSeriesTitle} from '../../mutations/helpers/createMeetingSeriesTitle' +import getKysely from '../../../postgres/getKysely' const startRetrospective: MutationResolvers['startRetrospective'] = async ( _source, @@ -118,7 +119,22 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( } IntegrationNotifier.startMeeting(dataLoader, meetingId, teamId) analytics.meetingStarted(viewer, meeting, template) - const {error} = await createGcalEvent({gcalInput, meetingId, teamId, viewerId, dataLoader}) + const {error, gcalSeriesId} = await createGcalEvent({ + gcalInput, + meetingId, + teamId, + viewerId, + rrule: recurrenceSettings?.rrule, + dataLoader + }) + if (meetingSeries && gcalSeriesId) { + const pg = getKysely() + await pg + .updateTable('MeetingSeries') + .set({gcalSeriesId}) + .where('id', '=', meetingSeries.id) + .execute() + } const data = {teamId, meetingId, hasGcalError: !!error?.message} publish(SubscriptionChannel.TEAM, teamId, 'StartRetrospectiveSuccess', data, subOptions) return data diff --git a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts index 335e08a1e84..daee90b7d61 100644 --- a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts +++ b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts @@ -12,6 +12,7 @@ import publish from '../../../utils/publish' import standardError from '../../../utils/standardError' import {MutationResolvers} from '../resolverTypes' import {MeetingTypeEnum} from '../../../postgres/types/Meeting' +import {updateGcalSeries} from '../../mutations/helpers/createGcalEvent' export const startNewMeetingSeries = async ( meeting: { @@ -111,6 +112,16 @@ const stopMeetingSeries = async (meetingSeries: MeetingSeries) => { .run() } +const updateGCalRecurrenceRule = (oldRule: RRule, newRule: RRule | null | undefined) => { + if (!newRule) { + return new RRule({ + ...oldRule.options, + until: new Date() + }) + } + return newRule +} + const updateRecurrenceSettings: MutationResolvers['updateRecurrenceSettings'] = async ( _source, {meetingId, recurrenceSettings}, @@ -138,6 +149,7 @@ const updateRecurrenceSettings: MutationResolvers['updateRecurrenceSettings'] = if (meeting.meetingSeriesId) { const meetingSeries = await dataLoader.get('meetingSeries').loadNonNull(meeting.meetingSeriesId) + const {gcalSeriesId, teamId, facilitatorId, recurrenceRule} = meetingSeries if (!recurrenceSettings.rrule) { await stopMeetingSeries(meetingSeries) @@ -146,6 +158,20 @@ const updateRecurrenceSettings: MutationResolvers['updateRecurrenceSettings'] = await updateMeetingSeries(meetingSeries, recurrenceSettings.rrule) analytics.recurrenceStarted(viewer, meetingSeries) } + if (gcalSeriesId) { + const rrule = updateGCalRecurrenceRule( + RRule.fromString(recurrenceRule), + recurrenceSettings.rrule + ) + await updateGcalSeries({ + gcalSeriesId, + title: recurrenceSettings.name ?? undefined, + rrule, + teamId, + userId: facilitatorId, + dataLoader + }) + } if (recurrenceSettings.name) { await updateMeetingSeriesQuery({title: recurrenceSettings.name}, meetingSeries.id) diff --git a/packages/server/graphql/public/typeDefs/Team.graphql b/packages/server/graphql/public/typeDefs/Team.graphql index 6a6d00c4c76..6e95b1c602f 100644 --- a/packages/server/graphql/public/typeDefs/Team.graphql +++ b/packages/server/graphql/public/typeDefs/Team.graphql @@ -86,7 +86,7 @@ type Team { """ true if the viewer is the team lead, else false """ - isLead: Boolean! + isViewerLead: Boolean! """ true if the viewer is an admin for the team's org, else false diff --git a/packages/server/graphql/types/Team.ts b/packages/server/graphql/types/Team.ts index 6c161bbcb35..7d737ad98b1 100644 --- a/packages/server/graphql/types/Team.ts +++ b/packages/server/graphql/types/Team.ts @@ -142,7 +142,7 @@ const Team: GraphQLObjectType = new GraphQLObjectType({ return dataLoader.get('teamInvitationsByTeamId').load(teamId) } }, - isLead: { + isViewerLead: { type: new GraphQLNonNull(GraphQLBoolean), description: 'true if the viewer is the team lead, else false', resolve: async ( diff --git a/packages/server/package.json b/packages/server/package.json index ddd4076d65c..c1bde9cbe3f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.16.0", + "version": "7.17.0", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -112,6 +112,7 @@ "ioredis": "^5.2.3", "jsdom": "^20.0.0", "jsonwebtoken": "^9.0.0", + "lodash.pick": "^4.4.0", "mailcomposer": "^4.0.1", "mailgun.js": "^9.3.0", "mime-types": "^2.1.16", @@ -123,7 +124,7 @@ "oauth-1.0a": "^2.2.6", "openai": "^4.24.1", "oy-vey": "^0.11.0", - "parabol-client": "7.16.0", + "parabol-client": "7.17.0", "pg": "^8.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/packages/server/postgres/migrations/1706021181176_addGCalEventToMeetingSeries.ts b/packages/server/postgres/migrations/1706021181176_addGCalEventToMeetingSeries.ts new file mode 100644 index 00000000000..919dda62a87 --- /dev/null +++ b/packages/server/postgres/migrations/1706021181176_addGCalEventToMeetingSeries.ts @@ -0,0 +1,22 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + ALTER TABLE "MeetingSeries" + ADD COLUMN IF NOT EXISTS "gcalSeriesId" VARCHAR(100); + `) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + ALTER TABLE "MeetingSeries" + DROP COLUMN IF EXISTS "gcalSeriesId"; + `) + await client.end() +} diff --git a/packages/server/utils/AtlassianServerManager.ts b/packages/server/utils/AtlassianServerManager.ts index d3ed48c166a..dcc29a4a3b9 100644 --- a/packages/server/utils/AtlassianServerManager.ts +++ b/packages/server/utils/AtlassianServerManager.ts @@ -387,6 +387,7 @@ class AtlassianServerManager extends AtlassianManager { })) projects.push(...pagedProjects) if (res.nextPage) { + log('AtlassianServerManager.getAllProjects fetching more results', res.total) return getProjectPage(cloudId, res.nextPage) } } @@ -395,7 +396,7 @@ class AtlassianServerManager extends AtlassianManager { cloudIds.map((cloudId) => getProjectPage( cloudId, - `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/project/search?orderBy=name` + `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/project/search?orderBy=name&maxResults=500` ) ) ) diff --git a/yarn.lock b/yarn.lock index 50e12881143..99d793dc367 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9012,9 +9012,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001517, caniuse-lite@~1.0.0: - version "1.0.30001584" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001584.tgz#5e3ea0625d048d5467670051687655b1f7bf7dfd" - integrity sha512-LOz7CCQ9M1G7OjJOF9/mzmqmj3jE/7VOmrfw6Mgs0E8cjOsbRXQJHsPBfmBOXDskXKrHLyyW3n7kpDW/4BsfpQ== + version "1.0.30001587" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz#a0bce920155fa56a1885a69c74e1163fc34b4881" + integrity sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA== capital-case@^1.0.4: version "1.0.4" @@ -12892,14 +12892,14 @@ ioredis@^5.2.3: standard-as-callback "^2.1.0" ip@^1.1.5, ip@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" - integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg== + version "1.1.9" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396" + integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ== ip@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" - integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105" + integrity sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ== ipaddr.js@1.9.1: version "1.9.1" @@ -14573,7 +14573,7 @@ lodash.mergewith@4.6.2: lodash.pick@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" - integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= + integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== lodash.setwith@^4.3.2: version "4.3.2" From 548542530f77a25d9658ff08587042e548d9c54b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:40:02 -0800 Subject: [PATCH 02/19] chore(release): Test v7.19.3 (#9469) * chore(env vars): Stripe vars moved to the Integrations section (#9427) * chore: fix misleading `isLead` field name on `Team` (#9413) * chore: fix misleading `isLead` field name on `Team` The field indicates whether the viewer is the lead, but when used in a query for a different user, the result could be read wrong. * Fix Team.isLead dependencies * feat: remove team template limit (#9424) * update error message and increase template limit * remove max team template limits * remove canClone prop from CloneTemplate * remove unused threshold * remove unused threshold * feat: Add Google calendar meeting series for recurrence (#9380) * feat: Add recurrence to GCal events * Fun with timezones * fix: Increase the number of projects fetched per request from Atlassian (#9435) We ran into timeouts in `getAllProjects`, presumably because we're doing too many roundtrips. As a quick fix, increse the number of projects fetched per request from 50 to 500. * chore(deps): bump ip from 1.1.8 to 1.1.9 (#9442) Bumps [ip](https://github.com/indutny/node-ip) from 1.1.8 to 1.1.9. - [Commits](https://github.com/indutny/node-ip/compare/v1.1.8...v1.1.9) --- updated-dependencies: - dependency-name: ip dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(release): release v7.17.0 (#9428) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * feat(standalone-deployment): Standalone host deployment improved and documented (#9445) * Docker compose stack improved * Remove unused containers from docker-compse and add useful comment on .env.example about PGSSLMODE * Docker compose profiles added. Documentation extended on how to use the profiles to manage the stack. * README fixed as docker compose up and down commands were not working * Typo fixed and docker-compose command replaced by docker compose * feat: support env-defined saml issuer for PPMIs (#9455) * feat: support env-defined saml issuer for PPMIs Signed-off-by: Matt Krick * feat: support single SAML for entire tenant Signed-off-by: Matt Krick --------- Signed-off-by: Matt Krick * chore: Associate logs with traces (#9444) * chore: Associate logs with traces Add trace information to log output for server side log statements. This does not include logging from code exclusively used for debugging, deploying or development. * Actually add the logger * Fix DD_LOGS_INJECTION check * chore(release): release v7.18.0 (#9450) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * chore: no force-push to prod (#9401) Signed-off-by: Matt Krick * chore(release): release v7.18.1 (#9459) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * feat: embedder service (#9417) * feat: add embedder service --------- Signed-off-by: Matt Krick Co-authored-by: Matt Krick * merge production to avoid force push (#9461) Signed-off-by: Matt Krick * chore(release): release v7.19.0 (#9460) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: checkout prod before merging it (#9463) Signed-off-by: Matt Krick * chore(release): release v7.19.1 (#9464) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: mrege origin/production strategy (#9465) Signed-off-by: Matt Krick * chore(release): release v7.19.2 (#9466) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: force push 5 (#9467) Signed-off-by: Matt Krick * chore(release): release v7.19.3 (#9468) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Signed-off-by: Matt Krick Co-authored-by: Rafa <101704572+rafaelromcar-parabol@users.noreply.github.com> Co-authored-by: Georg Bremer Co-authored-by: Nick O'Ferrall Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> Co-authored-by: Matt Krick Co-authored-by: Jordan Husney Co-authored-by: github-actions --- .env.example | 7 + .github/workflows/release-to-staging.yml | 5 + .release-please-manifest.json | 2 +- CHANGELOG.md | 48 ++ docker/dev.yml | 17 +- .../parabol-ubi/docker-host-st/.env.example | 1 + docker/parabol-ubi/docker-host-st/README.md | 33 +- .../docker-host-st/docker-compose.yaml | 156 +++- .../parabol-ubi/docker-host-st/postgres.conf | 1 - package.json | 6 +- packages/chronos/package.json | 4 +- packages/client/package.json | 2 +- packages/embedder/.eslintrc.js | 11 + packages/embedder/README.md | 71 ++ packages/embedder/ai_models/AbstractModel.ts | 75 ++ packages/embedder/ai_models/ModelManager.ts | 153 ++++ .../ai_models/TextEmbeddingsInference.ts | 71 ++ .../ai_models/TextGenerationInference.ts | 87 ++ .../ai_models/helpers/fetchWithRetry.ts | 65 ++ packages/embedder/embedder.ts | 253 ++++++ packages/embedder/indexing/countWords.ts | 17 + .../indexing/createEmbeddingTextFrom.ts | 15 + .../embedder/indexing/embeddingsTablesOps.ts | 198 +++++ packages/embedder/indexing/getRedisClient.ts | 11 + .../embedder/indexing/getRootDataLoader.ts | 10 + .../embedder/indexing/numberVectorToString.ts | 5 + .../indexing/orgIdsWithFeatureFlag.ts | 15 + .../indexing/retrospectiveDiscussionTopic.ts | 326 ++++++++ packages/embedder/package.json | 31 + packages/embedder/tsconfig.json | 15 + packages/gql-executor/package.json | 6 +- packages/integration-tests/package.json | 2 +- .../server/billing/helpers/adjustUserCount.ts | 3 +- .../billing/helpers/terminateSubscription.ts | 3 +- .../server/dataloader/azureDevOpsLoaders.ts | 15 +- .../server/dataloader/customLoaderMakers.ts | 10 +- .../server/dataloader/customRedisQueries.ts | 10 +- packages/server/fileStorage/GCSManager.ts | 3 +- .../server/graphql/mutations/endCheckIn.ts | 3 +- .../graphql/mutations/endSprintPoker.ts | 3 +- .../helpers/activatePrevSlackAuth.ts | 3 +- .../mutations/helpers/generateGroups.ts | 5 +- .../mutations/helpers/getCCFromCustomer.ts | 3 +- .../mutations/helpers/removeFromOrg.ts | 3 +- .../helpers/resolveDowngradeToStarter.ts | 3 +- .../mutations/helpers/safeEndRetrospective.ts | 3 +- .../mutations/helpers/safeEndTeamPrompt.ts | 3 +- .../server/graphql/mutations/moveTeamToOrg.ts | 3 +- .../graphql/mutations/navigateMeeting.ts | 3 +- .../graphql/mutations/selectTemplate.ts | 3 +- .../updateAzureDevOpsDimensionField.ts | 5 +- .../mutations/updateGitHubDimensionField.ts | 3 +- .../private/mutations/autopauseUsers.ts | 3 +- .../private/mutations/connectSocket.ts | 3 +- .../private/mutations/runScheduledJobs.ts | 7 +- .../mutations/acceptRequestToJoinDomain.ts | 3 +- .../SAMLHelpers/getURLWithSAMLRequestParam.ts | 3 +- .../mutations/updateGitLabDimensionField.ts | 3 +- .../graphql/public/types/TeamHealthStage.ts | 3 +- .../queries/helpers/fetchGitHubRepos.ts | 3 +- .../queries/helpers/fetchGitLabProjects.ts | 3 +- .../server/graphql/types/NewMeetingStage.ts | 3 +- packages/server/graphql/types/PokerMeeting.ts | 3 +- packages/server/package.json | 4 +- packages/server/postgres/getKysely.ts | 7 + .../1708127504000_updateEmbeddingMetadata.ts | 20 + .../upsertAzureDevOpsDimensionFieldMap.ts | 3 +- .../safeMutations/acceptTeamInvitation.ts | 3 +- packages/server/safetyPatchRes.ts | 17 +- .../server/utils/AtlassianServerManager.ts | 28 +- packages/server/utils/Logger.ts | 35 + packages/server/utils/OpenAIServerManager.ts | 5 +- .../server/utils/RecallAIServerManager.ts | 3 +- packages/server/utils/StaticServer.ts | 3 +- packages/server/utils/getSAMLURLFromEmail.ts | 20 + packages/server/utils/publish.ts | 3 +- packages/server/utils/stripe/StripeManager.ts | 3 +- pm2.config.js | 17 +- pm2.dev.config.js | 29 +- scripts/generateGraphQLArtifacts.js | 7 +- scripts/prod.js | 28 +- scripts/runEmbedder.js | 13 + scripts/webpack/dev.servers.config.js | 2 + scripts/webpack/prod.servers.config.js | 2 + scripts/webpack/toolbox.config.js | 20 + scripts/webpack/utils/transformRules.js | 3 +- yarn.lock | 759 +++++++++++++++++- 87 files changed, 2687 insertions(+), 193 deletions(-) delete mode 100644 docker/parabol-ubi/docker-host-st/postgres.conf create mode 100644 packages/embedder/.eslintrc.js create mode 100644 packages/embedder/README.md create mode 100644 packages/embedder/ai_models/AbstractModel.ts create mode 100644 packages/embedder/ai_models/ModelManager.ts create mode 100644 packages/embedder/ai_models/TextEmbeddingsInference.ts create mode 100644 packages/embedder/ai_models/TextGenerationInference.ts create mode 100644 packages/embedder/ai_models/helpers/fetchWithRetry.ts create mode 100644 packages/embedder/embedder.ts create mode 100644 packages/embedder/indexing/countWords.ts create mode 100644 packages/embedder/indexing/createEmbeddingTextFrom.ts create mode 100644 packages/embedder/indexing/embeddingsTablesOps.ts create mode 100644 packages/embedder/indexing/getRedisClient.ts create mode 100644 packages/embedder/indexing/getRootDataLoader.ts create mode 100644 packages/embedder/indexing/numberVectorToString.ts create mode 100644 packages/embedder/indexing/orgIdsWithFeatureFlag.ts create mode 100644 packages/embedder/indexing/retrospectiveDiscussionTopic.ts create mode 100644 packages/embedder/package.json create mode 100644 packages/embedder/tsconfig.json create mode 100644 packages/server/postgres/migrations/1708127504000_updateEmbeddingMetadata.ts create mode 100644 packages/server/utils/Logger.ts create mode 100644 scripts/runEmbedder.js diff --git a/.env.example b/.env.example index 88cdde703de..0d0a44b41f0 100644 --- a/.env.example +++ b/.env.example @@ -8,12 +8,19 @@ SERVER_ID='1' # Websocket port for the websocket server, only used in development (yarn dev) SOCKET_PORT='3001' +# AI MODELS +AI_EMBEDDING_MODELS='[{"model": "text-embeddings-inference:llmrails/ember-v1", "url": "http://localhost:3040/"}]' +AI_GENERATION_MODELS='[{"model": "text-generation-inference:TheBloke/zephyr-7b-beta", "url": "http://localhost:3050/"}]' +AI_EMBEDDER_ENABLED='true' + # APPLICATION # AMPLITUDE_WRITE_KEY='key_AMPLITUDE_WRITE_KEY' # Enter a short url redirect service for invitations, it needs to redirecto to /invitation-link # INVITATION_SHORTLINK='example.com' # If true, all new orgs will default to being enterprise tier. Use for PPMIs # IS_ENTERPRISE=false +# PPMI single tenant use only. Will set the SAML issuer to this value. +# SAML_ISSUER='' # AUTHENTICATION # AUTH_INTERNAL_DISABLED='false' diff --git a/.github/workflows/release-to-staging.yml b/.github/workflows/release-to-staging.yml index 139b8472fa8..c6e3601f2ff 100644 --- a/.github/workflows/release-to-staging.yml +++ b/.github/workflows/release-to-staging.yml @@ -14,6 +14,10 @@ jobs: id-token: "write" pull-requests: "write" steps: + - name: Checkout production + uses: actions/checkout@v3 + with: + ref: production - name: Checkout uses: actions/checkout@v3 - name: Setup environment variables @@ -99,6 +103,7 @@ jobs: git config user.name github-actions git config user.email github-actions@github.com git checkout -b "release/v${{ env.ACTION_VERSION }}" + git merge -s ours origin/production --allow-unrelated-histories git push --set-upstream origin "release/v${{ env.ACTION_VERSION }}" gh pr create \ --assignee ${{ github.actor }} \ diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 0fb11cbd996..517aeb7a63e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.17.0" + ".": "7.19.3" } diff --git a/CHANGELOG.md b/CHANGELOG.md index fd68e143309..579182dc3f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,54 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.19.3](https://github.com/ParabolInc/parabol/compare/v7.19.2...v7.19.3) (2024-02-28) + + +### Fixed + +* force push 5 ([#9467](https://github.com/ParabolInc/parabol/issues/9467)) ([581f0cf](https://github.com/ParabolInc/parabol/commit/581f0cfa2255bbeb438c53b2b5f4d8ceb6a0b0cc)) + +## [7.19.2](https://github.com/ParabolInc/parabol/compare/v7.19.1...v7.19.2) (2024-02-28) + + +### Fixed + +* mrege origin/production strategy ([#9465](https://github.com/ParabolInc/parabol/issues/9465)) ([9e90b9d](https://github.com/ParabolInc/parabol/commit/9e90b9df95b8505c0e1e50d4e8e4f18c73ef17cd)) + +## [7.19.1](https://github.com/ParabolInc/parabol/compare/v7.19.0...v7.19.1) (2024-02-27) + + +### Fixed + +* checkout prod before merging it ([#9463](https://github.com/ParabolInc/parabol/issues/9463)) ([7bd8803](https://github.com/ParabolInc/parabol/commit/7bd880314f6f48c897a9a708b2d6435b257fae90)) + +## [7.19.0](https://github.com/ParabolInc/parabol/compare/v7.18.1...v7.19.0) (2024-02-27) + + +### Added + +* embedder service ([#9417](https://github.com/ParabolInc/parabol/issues/9417)) ([55faa17](https://github.com/ParabolInc/parabol/commit/55faa17ada5b1bd49182a29341b3465a82d2ddfd)) + +## [7.18.1](https://github.com/ParabolInc/parabol/compare/v7.18.0...v7.18.1) (2024-02-27) + + +### Changed + +* no force-push to prod ([#9401](https://github.com/ParabolInc/parabol/issues/9401)) ([6d46e1b](https://github.com/ParabolInc/parabol/commit/6d46e1b2aab6731493de2d2547c88ae3921393f0)) + +## [7.18.0](https://github.com/ParabolInc/parabol/compare/v7.17.0...v7.18.0) (2024-02-27) + + +### Added + +* **standalone-deployment:** Standalone host deployment improved and documented ([#9445](https://github.com/ParabolInc/parabol/issues/9445)) ([61ba015](https://github.com/ParabolInc/parabol/commit/61ba015c8310a72b7e89c64be081cd2f399fc721)) +* support env-defined saml issuer for PPMIs ([#9455](https://github.com/ParabolInc/parabol/issues/9455)) ([92ab5be](https://github.com/ParabolInc/parabol/commit/92ab5be298ceb19ca8718c67a0c9da8728b6b0bf)) + + +### Changed + +* Associate logs with traces ([#9444](https://github.com/ParabolInc/parabol/issues/9444)) ([c77925b](https://github.com/ParabolInc/parabol/commit/c77925b1c0e07afc428022008143b8b7f4002280)) + ## [7.17.0](https://github.com/ParabolInc/parabol/compare/v7.16.0...v7.17.0) (2024-02-21) diff --git a/docker/dev.yml b/docker/dev.yml index 0b7de79ddb8..c45ba6b6f6b 100644 --- a/docker/dev.yml +++ b/docker/dev.yml @@ -13,8 +13,6 @@ services: - /var/run/docker.sock:/var/run/docker.sock - /proc/:/host/proc/:ro - /sys/fs/cgroup:/host/sys/fs/cgroup:ro - - "./dd-conf.d:/etc/datadog-agent/conf.d/local.d/" - - "../dev/logs:/var/log/datadog/logs" db: image: rethinkdb:2.4.2 restart: unless-stopped @@ -72,6 +70,20 @@ services: - "8082:8081" networks: parabol-network: + text-embeddings-inference: + container_name: text-embeddings-inference + image: ghcr.io/huggingface/text-embeddings-inference:cpu-0.6 + command: + - "--model-id=llmrails/ember-v1" + platform: linux/x86_64 + hostname: text-embeddings-inference + restart: unless-stopped + ports: + - "3040:80" + volumes: + - text-embeddings-inference-data:/data + networks: + parabol-network: networks: parabol-network: volumes: @@ -79,3 +91,4 @@ volumes: rethink-data: {} postgres-data: {} pgadmin-data: {} + text-embeddings-inference-data: {} diff --git a/docker/parabol-ubi/docker-host-st/.env.example b/docker/parabol-ubi/docker-host-st/.env.example index 82c7b2b7e29..9eadf7108e5 100644 --- a/docker/parabol-ubi/docker-host-st/.env.example +++ b/docker/parabol-ubi/docker-host-st/.env.example @@ -1 +1,2 @@ # See https://github.com/ParabolInc/parabol/blob/master/.env.example +# DO NOT SET PGSSLMODE to an empty value. Postgres will not be able to start. diff --git a/docker/parabol-ubi/docker-host-st/README.md b/docker/parabol-ubi/docker-host-st/README.md index c743187aa1d..af3a0b244ec 100644 --- a/docker/parabol-ubi/docker-host-st/README.md +++ b/docker/parabol-ubi/docker-host-st/README.md @@ -1,15 +1,42 @@ # Docker Host Single Tenant (ST) -To run the Parabol UBI in single tenant mode (e.g. simple docker-compose on a docker host). +To run Parabol in single tenant mode (e.g. simple docker-compose on a docker host). 1. Build your Parabol UBI using instructions in `docker/ubi/docker-build/README.md` 2. Create a working `.env` from `.env.example` 3. Update docker-compose.yaml `image: #image:tag` with your built image tag from `step (1.)` -4. Run `docker-compose up -d` to deploy the local stack. You can run `docker-compose down` to terminate the local stack -5. Check logs via command `docker logs -app-1` and wait for the following output to appear +4. Run `docker compose --profile databases --profile parabol up -d` to deploy the local stack. You can run `docker compose --profile databases --profile parabol down` to terminate the local stack +5. Check logs via command `docker logs -f` and wait for the following output to appear ```shell 🔥🔥🔥 Server ID: 0. Ready for Sockets: Port 3000 🔥🔥🔥 💧💧💧 Server ID: 0. Ready for GraphQL Execution 💧💧💧 💧💧💧 Server ID: 01. Ready for GraphQL Execution 💧💧💧 ``` + +## Upgrade Parabol version + +1. Edit the `docker-compose.yaml` and change the `#image:tag` changing the tag. Ex: from `v7.15.0` to `v7.15.2`. +2. (optional) In a different terminal, run `docker compose logs -f` to follow the upgrade. +3. Run `docker compose --profile databases --profile parabol up -d`. It will start the `pre-deploy` and, once it is done successfully, then it will stop and recreate the `web-server` and `gql-executor` with the new version of the image. **This step implies a downtime**. +4. Verify the application is still up and running. + +## Running Chronos + +Chronos isn't started by default. If it needs to run, it must be managed using `docker compose --profile databases --profile parabol --profile chronos up`. + +This will run `pre-deploy` and thus it will recreate the `web-server` and the `gql-executor`. + +## Database debug + +Some tools are available to debug the databases is needed: + +- pgadmin +- redis-commander + +To operate them use `docker compose up --profile databases --profile database-debug`. + +## Running the whole stack + +- Start the whole stack: `docker compose --profile databases --profile parabol --profile database-debug --profile chronos up -d`. +- Stop the stack: `docker compose --profile databases --profile parabol --profile database-debug --profile chronos down` diff --git a/docker/parabol-ubi/docker-host-st/docker-compose.yaml b/docker/parabol-ubi/docker-host-st/docker-compose.yaml index c7ff752a95b..52bb8c76a7d 100644 --- a/docker/parabol-ubi/docker-host-st/docker-compose.yaml +++ b/docker/parabol-ubi/docker-host-st/docker-compose.yaml @@ -1,51 +1,165 @@ -version: '3.7' +version: '3.9' services: - db: - image: rethinkdb:latest + postgres: + container_name: postgres + profiles: ["databases"] + image: postgres:15.4 + restart: always + env_file: .env + environment: + - PGUSER=$POSTGRES_USER + ports: + - '5432:5432' + volumes: + - './data/postgres/pgdata:/var/lib/postgresql/data' + healthcheck: + test: ["CMD-SHELL", "pg_isready", "-d", "$POSTGRES_DB", "-U", "$POSTGRES_USER"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - parabol-network + pgadmin: + profiles: ["database-debug"] + container_name: pgadmin + image: dpage/pgadmin4:8.3 + depends_on: + postgres: + condition: service_healthy + env_file: .env + ports: + - "5050:80" + networks: + - parabol-network + rethinkdb: + container_name: rethinkdb + profiles: ["databases"] + image: rethinkdb:2.4.2 restart: always ports: - '8080:8080' - '29015:29015' - '28015:28015' volumes: - - ./rethink-data:/data + - ./data/rethink:/data networks: - parabol-network - postgres: - image: postgres:15.4 + redis: + container_name: redis + profiles: ["databases"] + image: redis:7.0-alpine + healthcheck: + test: "[ $$(redis-cli ping) = 'PONG' ]" + interval: 10s + timeout: 5s + retries: 5 restart: always - env_file: .env ports: - - '5432:5432' + - '6379:6379' volumes: - - './postgres.conf:/usr/local/etc/postgres/postgres.conf' - - './postgres-data/pgdata:/var/lib/postgresql/data' - command: 'postgres -c config_file=/usr/local/etc/postgres/postgres.conf' + - ./data/redis:/data networks: - parabol-network - redis: - image: redis + redis-commander: + profiles: ["database-debug"] + container_name: redis-commander + image: ghcr.io/joeferner/redis-commander:0.8.1 + depends_on: + redis: + condition: service_healthy restart: always + environment: + - REDIS_HOSTS=local:redis:6379 ports: - - '6379:6379' + - "8081:8081" + networks: + - parabol-network + pre-deploy: + container_name: pre-deploy + profiles: ["parabol"] + image: us-central1-docker.pkg.dev/prbl-tooling/parabol-production/parabol:v7.15.2 + command: bash -c "node dist/preDeploy.js" + env_file: .env + environment: + - SERVER_ID=0 volumes: - - ./redis-data:/data + - './.env:/parabol/.env' + depends_on: + rethinkdb: + condition: service_started + postgres: + condition: service_healthy + redis: + condition: service_healthy networks: - parabol-network - app: - image: #image:tag + chronos: + container_name: chronos + profiles: ["chronos"] + image: us-central1-docker.pkg.dev/prbl-tooling/parabol-production/parabol:v7.15.2 restart: always + command: bash -c "node dist/chronos.js" env_file: .env - command: bash -c "yarn predeploy && NODE_ENV=production && yarn start" + environment: + - SERVER_ID=1 + volumes: + - './.env:/parabol/.env' + depends_on: + pre-deploy: + condition: service_completed_successfully + rethinkdb: + condition: service_started + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - parabol-network + web-server: + container_name: web-server + profiles: ["parabol"] + image: us-central1-docker.pkg.dev/prbl-tooling/parabol-production/parabol:v7.15.2 + restart: always + command: bash -c "node dist/web.js" + env_file: .env + environment: + - SERVER_ID=5 ports: - '3000:3000' volumes: - './.env:/parabol/.env' depends_on: - - db - - redis - - postgres + pre-deploy: + condition: service_completed_successfully + rethinkdb: + condition: service_started + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - parabol-network + gql-executor: + container_name: gql-executor + profiles: ["parabol"] + image: us-central1-docker.pkg.dev/prbl-tooling/parabol-production/parabol:v7.15.2 + restart: always + command: bash -c "node dist/gqlExecutor.js" + env_file: .env + environment: + - SERVER_ID=10 + volumes: + - './.env:/parabol/.env' + depends_on: + pre-deploy: + condition: service_completed_successfully + rethinkdb: + condition: service_started + postgres: + condition: service_healthy + redis: + condition: service_healthy networks: - parabol-network networks: diff --git a/docker/parabol-ubi/docker-host-st/postgres.conf b/docker/parabol-ubi/docker-host-st/postgres.conf deleted file mode 100644 index 3357fd28574..00000000000 --- a/docker/parabol-ubi/docker-host-st/postgres.conf +++ /dev/null @@ -1 +0,0 @@ -listen_addresses='*' \ No newline at end of file diff --git a/package.json b/package.json index edde1656431..cc2eb303c5d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.17.0", + "version": "7.19.3", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -103,8 +103,8 @@ "html-webpack-plugin": "^5.5.0", "husky": "^7.0.4", "jscodeshift": "^0.14.0", - "kysely": "^0.26.3", - "kysely-codegen": "^0.10.0", + "kysely": "^0.27.2", + "kysely-codegen": "^0.11.0", "lerna": "^6.4.1", "mini-css-extract-plugin": "^2.7.2", "minimist": "^1.2.5", diff --git a/packages/chronos/package.json b/packages/chronos/package.json index 5295ced9dd0..ff73a275826 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.17.0", + "version": "7.19.3", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -25,6 +25,6 @@ }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.17.0" + "parabol-server": "7.19.3" } } diff --git a/packages/client/package.json b/packages/client/package.json index 9faef2216e4..364dbda0932 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.17.0", + "version": "7.19.3", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/embedder/.eslintrc.js b/packages/embedder/.eslintrc.js new file mode 100644 index 00000000000..a6a5d110f1e --- /dev/null +++ b/packages/embedder/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + extends: [ + '../../.eslintrc.js' + ], + parserOptions: { + project: './tsconfig.json', + ecmaVersion: 2020, + sourceType: 'module' + }, + "ignorePatterns": ["**/lib", "*.js"] +} diff --git a/packages/embedder/README.md b/packages/embedder/README.md new file mode 100644 index 00000000000..fc3fc68f335 --- /dev/null +++ b/packages/embedder/README.md @@ -0,0 +1,71 @@ +# `Embedder` + +This service builds embedding vectors for semantic search and for other AI/ML +use cases. It does so by: + +1. Updating a list of all possible items to create embedding vectors for and + storing that list in the `EmbeddingsMetadata` table +2. Adding these items in batches to the `EmbeddingsJobQueue` table and a redis + priority queue called `embedder:queue` +3. Allowing one or more parallel embedding services to calculate embedding + vectors (EmbeddingJobQueue states transistion from `queued` -> `embedding`, + then `embedding` -> [deleting the `EmbeddingJobQueue` row] + + In addition to deleteing the `EmbeddingJobQueue` row, when a job completes + successfully: + + - A row is added to the model table with the embedding vector; the + `EmbeddingMetadataId` field on this row points the appropriate + metadata row on `EmbeddingsMetadata` + - The `EmbeddingsMetadata.models` array is updated with the name of the + table that the embedding has been generated for + +4. This process repeats forever using a silly polling loop + +In the future, it would be wonderful to enhance this service such that it were +event driven. + +## Prerequisites + +The Embedder service depends on pgvector being available in Postgres. + +The predeploy script checks for an environment variable +`POSTGRES_USE_PGVECTOR=true` to enable this extension in production. + +## Configuration + +The Embedder service takes no arguments and is controlled by the following +environment variables, here given with example configuration: + +- `AI_EMBEDDER_ENABLE`: enable/disable the embedder service from + performing work, or sleeping indefinitely + +`AI_EMBEDDER_ENABLED='true'` + +- `AI_EMBEDDING_MODELS`: JSON configuration for which embedding models + are enabled. Each model in the array will be instantiated by + `ai_models/ModelManager`. Each model instance will have its own + database table created for it (if it does not exist already) used + to store calculated vectors. See `ai_models/ModelManager` for + which configurations are supported. + + Example: + +`AI_EMBEDDING_MODELS='[{"model": "text-embeddings-inference:llmrails/ember-v1", "url": "http://localhost:3040/"}]'` + +- `AI_GENERATION_MODELS`: JSON configuration for which AI generation + models (i.e. GPTS are enabled). These models are used for summarization + text to be embedded by an embedding model if the text length would be + greater than the context window of the embedding model. Each model in + the array will be instantiated by `ai_models/ModelManager`. + See `ai_models/ModelManager` for which configurations are supported. + + Example: + +`AI_GENERATION_MODELS='[{"model": "text-generation-inference:TheBloke/zephyr-7b-beta", "url": "http://localhost:3050/"}]'` + +## Usage + +The Embedder service is stateless and takes no arguments. Multiple instances +of the service may be started in order to match embedding load, or to +catch up on history more quickly. diff --git a/packages/embedder/ai_models/AbstractModel.ts b/packages/embedder/ai_models/AbstractModel.ts new file mode 100644 index 00000000000..b0f709ce485 --- /dev/null +++ b/packages/embedder/ai_models/AbstractModel.ts @@ -0,0 +1,75 @@ +export interface ModelConfig { + model: string + url: string +} + +export interface EmbeddingModelConfig extends ModelConfig { + tableSuffix: string +} + +export interface GenerationModelConfig extends ModelConfig {} + +export abstract class AbstractModel { + public readonly url?: string + public modelInstance: any + + constructor(config: ModelConfig) { + this.url = this.normalizeUrl(config.url) + } + + // removes a trailing slash from the inputUrl + private normalizeUrl(inputUrl: string | undefined) { + if (!inputUrl) return undefined + const regex = /[/]+$/ + return inputUrl.replace(regex, '') + } +} + +export interface EmbeddingModelParams { + embeddingDimensions: number + maxInputTokens: number + tableSuffix: string +} + +export abstract class AbstractEmbeddingsModel extends AbstractModel { + readonly embeddingDimensions: number + readonly maxInputTokens: number + readonly tableName: string + constructor(config: EmbeddingModelConfig) { + super(config) + const modelParams = this.constructModelParams(config) + this.embeddingDimensions = modelParams.embeddingDimensions + this.maxInputTokens = modelParams.maxInputTokens + this.tableName = `Embeddings_${modelParams.tableSuffix}` + } + protected abstract constructModelParams(config: EmbeddingModelConfig): EmbeddingModelParams + abstract getEmbedding(content: string): Promise +} + +export interface GenerationModelParams { + maxInputTokens: number +} + +export interface GenerationOptions { + maxNewTokens?: number + seed?: number + stop?: string + temperature?: number + topK?: number + topP?: number + truncate?: boolean +} + +export abstract class AbstractGenerationModel extends AbstractModel { + readonly maxInputTokens: number + constructor(config: GenerationModelConfig) { + super(config) + const modelParams = this.constructModelParams(config) + this.maxInputTokens = modelParams.maxInputTokens + } + + protected abstract constructModelParams(config: GenerationModelConfig): GenerationModelParams + abstract summarize(content: string, options: GenerationOptions): Promise +} + +export default AbstractModel diff --git a/packages/embedder/ai_models/ModelManager.ts b/packages/embedder/ai_models/ModelManager.ts new file mode 100644 index 00000000000..ac8f04cc891 --- /dev/null +++ b/packages/embedder/ai_models/ModelManager.ts @@ -0,0 +1,153 @@ +import {Kysely, sql} from 'kysely' + +import { + AbstractEmbeddingsModel, + AbstractGenerationModel, + EmbeddingModelConfig, + GenerationModelConfig, + ModelConfig +} from './AbstractModel' +import TextEmbeddingsInference from './TextEmbeddingsInference' +import TextGenerationInference from './TextGenerationInference' + +interface ModelManagerConfig { + embeddingModels: EmbeddingModelConfig[] + generationModels: GenerationModelConfig[] +} + +export type EmbeddingsModelType = 'text-embeddings-inference' +export type GenerationModelType = 'text-generation-inference' + +export class ModelManager { + embeddingModels: AbstractEmbeddingsModel[] + embeddingModelsMapByTable: {[key: string]: AbstractEmbeddingsModel} + generationModels: AbstractGenerationModel[] + + private isValidConfig( + maybeConfig: Partial + ): maybeConfig is ModelManagerConfig { + if (!maybeConfig.embeddingModels || !Array.isArray(maybeConfig.embeddingModels)) { + throw new Error('Invalid configuration: embedding_models is missing or not an array') + } + if (!maybeConfig.generationModels || !Array.isArray(maybeConfig.generationModels)) { + throw new Error('Invalid configuration: summarization_models is missing or not an array') + } + + maybeConfig.embeddingModels.forEach((model: ModelConfig) => { + this.isValidModelConfig(model) + }) + + maybeConfig.generationModels.forEach((model: ModelConfig) => { + this.isValidModelConfig(model) + }) + + return true + } + + private isValidModelConfig(model: ModelConfig): model is ModelConfig { + if (typeof model.model !== 'string') { + throw new Error('Invalid ModelConfig: model field should be a string') + } + if (model.url !== undefined && typeof model.url !== 'string') { + throw new Error('Invalid ModelConfig: url field should be a string') + } + + return true + } + + constructor(config: ModelManagerConfig) { + // Validate configuration + this.isValidConfig(config) + + // Initialize embeddings models + this.embeddingModelsMapByTable = {} + this.embeddingModels = config.embeddingModels.map((modelConfig) => { + const [modelType] = modelConfig.model.split(':') as [EmbeddingsModelType, string] + + switch (modelType) { + case 'text-embeddings-inference': { + const embeddingsModel = new TextEmbeddingsInference(modelConfig) + this.embeddingModelsMapByTable[embeddingsModel.tableName] = embeddingsModel + return embeddingsModel + } + default: + throw new Error(`unsupported embeddings model '${modelType}'`) + } + }) + + // Initialize summarization models + this.generationModels = config.generationModels.map((modelConfig) => { + const [modelType, _] = modelConfig.model.split(':') as [GenerationModelType, string] + + switch (modelType) { + case 'text-generation-inference': { + const generator = new TextGenerationInference(modelConfig) + return generator + } + default: + throw new Error(`unsupported summarization model '${modelType}'`) + } + }) + } + + async maybeCreateTables(pg: Kysely) { + const maybePromises = this.embeddingModels.map(async (embeddingsModel) => { + const tableName = embeddingsModel.tableName + const hasTable = + ( + await sql`SELECT 1 FROM ${sql.id('pg_catalog', 'pg_tables')} WHERE ${sql.id( + 'tablename' + )} = ${tableName}`.execute(pg) + ).rows.length > 0 + if (hasTable) return undefined + const vectorDimensions = embeddingsModel.embeddingDimensions + console.log(`ModelManager: creating ${tableName} with ${vectorDimensions} dimensions`) + const query = sql` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS ${sql.id(tableName)} ( + "id" INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "embedText" TEXT, + "embedding" vector(${sql.raw(vectorDimensions.toString())}), + "embeddingsMetadataId" INTEGER NOT NULL, + FOREIGN KEY ("embeddingsMetadataId") + REFERENCES "EmbeddingsMetadata"("id") + ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS "idx_${sql.raw(tableName)}_embedding_vector_cosign_ops" + ON ${sql.id(tableName)} + USING hnsw ("embedding" vector_cosine_ops); + END $$; + + ` + return query.execute(pg) + }) + Promise.all(maybePromises) + } +} + +let modelManager: ModelManager | undefined +export function getModelManager() { + if (modelManager) return modelManager + const {AI_EMBEDDING_MODELS, AI_GENERATION_MODELS} = process.env + const config: ModelManagerConfig = { + embeddingModels: [], + generationModels: [] + } + try { + config.embeddingModels = AI_EMBEDDING_MODELS && JSON.parse(AI_EMBEDDING_MODELS) + } catch (e) { + throw new Error(`Invalid AI_EMBEDDING_MODELS .env JSON: ${e}`) + } + try { + config.generationModels = AI_GENERATION_MODELS && JSON.parse(AI_GENERATION_MODELS) + } catch (e) { + throw new Error(`Invalid AI_GENERATION_MODELS .env JSON: ${e}`) + } + + modelManager = new ModelManager(config) + + return modelManager +} + +export default getModelManager diff --git a/packages/embedder/ai_models/TextEmbeddingsInference.ts b/packages/embedder/ai_models/TextEmbeddingsInference.ts new file mode 100644 index 00000000000..93bb2c88c2f --- /dev/null +++ b/packages/embedder/ai_models/TextEmbeddingsInference.ts @@ -0,0 +1,71 @@ +import {AbstractEmbeddingsModel, EmbeddingModelConfig, EmbeddingModelParams} from './AbstractModel' +import fetchWithRetry from './helpers/fetchWithRetry' + +const MAX_REQUEST_TIME_S = 3 * 60 + +export type ModelId = 'BAAI/bge-large-en-v1.5' | 'llmrails/ember-v1' + +const modelIdDefinitions: Record = { + 'BAAI/bge-large-en-v1.5': { + embeddingDimensions: 1024, + maxInputTokens: 512, + tableSuffix: 'bge_l_en_1p5' + }, + 'llmrails/ember-v1': { + embeddingDimensions: 1024, + maxInputTokens: 512, + tableSuffix: 'ember_1' + } +} + +function isValidModelId(object: any): object is ModelId { + return Object.keys(modelIdDefinitions).includes(object) +} + +export class TextEmbeddingsInference extends AbstractEmbeddingsModel { + constructor(config: EmbeddingModelConfig) { + super(config) + } + + public async getEmbedding(content: string) { + const fetchOptions = { + body: JSON.stringify({inputs: content}), + deadline: new Date(new Date().getTime() + MAX_REQUEST_TIME_S * 1000), + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json; charset=utf-8' + }, + method: 'POST' + } + + try { + const res = await fetchWithRetry(`${this.url}/embed`, fetchOptions) + const listOfVectors = (await res.json()) as Array + if (!listOfVectors) + throw new Error('TextEmbeddingsInference.getEmbeddings(): listOfVectors is undefined') + if (listOfVectors.length !== 1 || !listOfVectors[0]) + throw new Error( + `TextEmbeddingsInference.getEmbeddings(): listOfVectors list length !== 1 (length: ${listOfVectors.length})` + ) + return listOfVectors[0] + } catch (e) { + console.log(`TextEmbeddingsInference.getEmbeddings() timeout: `, e) + throw e + } + } + + protected constructModelParams(config: EmbeddingModelConfig): EmbeddingModelParams { + const modelConfigStringSplit = config.model.split(':') + if (modelConfigStringSplit.length != 2) { + throw new Error('TextGenerationInference model string must be colon-delimited and len 2') + } + + if (!this.url) throw new Error('TextGenerationInferenceSummarizer model requires url') + const maybeModelId = modelConfigStringSplit[1] + if (!isValidModelId(maybeModelId)) + throw new Error(`TextGenerationInference model subtype unknown: ${maybeModelId}`) + return modelIdDefinitions[maybeModelId] + } +} + +export default TextEmbeddingsInference diff --git a/packages/embedder/ai_models/TextGenerationInference.ts b/packages/embedder/ai_models/TextGenerationInference.ts new file mode 100644 index 00000000000..6f12ce09974 --- /dev/null +++ b/packages/embedder/ai_models/TextGenerationInference.ts @@ -0,0 +1,87 @@ +import { + AbstractGenerationModel, + GenerationModelConfig, + GenerationModelParams, + GenerationOptions +} from './AbstractModel' +import fetchWithRetry from './helpers/fetchWithRetry' + +const MAX_REQUEST_TIME_S = 3 * 60 + +export type ModelId = 'TheBloke/zephyr-7b-beta' + +const modelIdDefinitions: Record = { + 'TheBloke/zephyr-7b-beta': { + maxInputTokens: 512 + } +} + +function isValidModelId(object: any): object is ModelId { + return Object.keys(modelIdDefinitions).includes(object) +} + +export class TextGenerationInference extends AbstractGenerationModel { + constructor(config: GenerationModelConfig) { + super(config) + } + + public async summarize(content: string, options: GenerationOptions) { + const { + maxNewTokens: max_new_tokens = 512, + seed, + stop, + temperature = 0.8, + topP, + topK, + truncate + } = options + const parameters = { + max_new_tokens, + seed, + stop, + temperature, + topP, + topK, + truncate + } + const prompt = `Create a brief, one-paragraph summary of the following: ${content}` + const fetchOptions = { + body: JSON.stringify({ + inputs: prompt, + parameters + }), + deadline: new Date(new Date().getTime() + MAX_REQUEST_TIME_S * 1000), + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json; charset=utf-8' + }, + method: 'POST' + } + + try { + // console.log(`TextGenerationInterface.summarize(): summarizing from ${this.url}/generate`) + const res = await fetchWithRetry(`${this.url}/generate`, fetchOptions) + const json = await res.json() + if (!json || !json.generated_text) + throw new Error('TextGenerationInterface.summarize(): malformed response') + return json.generated_text as string + } catch (e) { + console.log('TextGenerationInterfaceSummarizer.summarize(): timeout') + throw e + } + } + protected constructModelParams(config: GenerationModelConfig): GenerationModelParams { + const modelConfigStringSplit = config.model.split(':') + if (modelConfigStringSplit.length != 2) { + throw new Error('TextGenerationInterface model string must be colon-delimited and len 2') + } + + if (!this.url) throw new Error('TextGenerationInterfaceSummarizer model requires url') + const maybeModelId = modelConfigStringSplit[1] + if (!isValidModelId(maybeModelId)) + throw new Error(`TextGenerationInterface model subtype unknown: ${maybeModelId}`) + return modelIdDefinitions[maybeModelId] + } +} + +export default TextGenerationInference diff --git a/packages/embedder/ai_models/helpers/fetchWithRetry.ts b/packages/embedder/ai_models/helpers/fetchWithRetry.ts new file mode 100644 index 00000000000..98343ef26e6 --- /dev/null +++ b/packages/embedder/ai_models/helpers/fetchWithRetry.ts @@ -0,0 +1,65 @@ +interface FetchWithRetryOptions extends RequestInit { + deadline: Date // Deadline for the request to complete + debug?: boolean // Enable debug tracing + retryStatusCodes?: number[] // Array of status codes to retry on +} + +export default async (url: RequestInfo, options: FetchWithRetryOptions): Promise => { + const {deadline, debug = false, retryStatusCodes = [429], ...fetchOptions} = options + let attempt = 0 + const controller = new AbortController() + fetchOptions.signal = controller.signal + + const timeout = deadline.getTime() - Date.now() + if (timeout <= 0) { + throw new Error('Deadline has already passed') + } + + const timeoutId = setTimeout(() => controller.abort(), timeout) + + try { + while (Date.now() < deadline.getTime()) { + attempt++ + + if (debug) { + console.log(`Attempt ${attempt}: Fetching ${url}`) + } + + const response = await fetch(url, fetchOptions) + + if (!retryStatusCodes.includes(response.status)) { + clearTimeout(timeoutId) + return response + } + + const retryAfter = response.headers.get('Retry-After') + // if Retry-After specified, use it; else fallback to exponential backoff + let waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.pow(2, attempt) * 1000 + + // cap waitTime to prevent exceeding the deadline + waitTime = Math.min(waitTime, deadline.getTime() - Date.now()) + + if (debug) { + console.log( + `Waiting ${waitTime / 1000} seconds before retrying due to status ${response.status}...` + ) + } + await new Promise((resolve) => setTimeout(resolve, waitTime)) + } + + throw new Error('Deadline exceeded') + } catch (error) { + clearTimeout(timeoutId) + if (error instanceof Error && error.name === 'AbortError') { + throw new Error('Request aborted due to deadline') + } + if (debug) { + console.error(`Attempt ${attempt} failed: ${error}`) + } + const currentTime = Date.now() + if (currentTime >= deadline.getTime()) { + throw new Error('Deadline exceeded before a successful request') + } + throw error // Re-throw the error if it's not related to deadline exceeding + } +} diff --git a/packages/embedder/embedder.ts b/packages/embedder/embedder.ts new file mode 100644 index 00000000000..1072a546d00 --- /dev/null +++ b/packages/embedder/embedder.ts @@ -0,0 +1,253 @@ +import {Insertable} from 'kysely' +import tracer from 'dd-trace' +import Redlock, {RedlockAbortSignal} from 'redlock' + +import 'parabol-server/initSentry' +import getKysely from 'parabol-server/postgres/getKysely' +import {DB} from 'parabol-server/postgres/pg' +import {refreshRetroDiscussionTopicsMeta as refreshRetroDiscussionTopicsMeta} from './indexing/retrospectiveDiscussionTopic' +import {orgIdsWithFeatureFlag} from './indexing/orgIdsWithFeatureFlag' +import getModelManager, {ModelManager} from './ai_models/ModelManager' +import {countWords} from './indexing/countWords' +import {createEmbeddingTextFrom} from './indexing/createEmbeddingTextFrom' +import { + selectJobQueueItemById, + selectMetadataByJobQueueId, + updateJobState +} from './indexing/embeddingsTablesOps' +import {selectMetaToQueue} from './indexing/embeddingsTablesOps' +import {insertNewJobs} from './indexing/embeddingsTablesOps' +import {completeJobTxn} from './indexing/embeddingsTablesOps' +import {getRootDataLoader} from './indexing/getRootDataLoader' +import {getRedisClient} from './indexing/getRedisClient' + +/* + * TODO List + * - [ ] implement a clean-up function that re-queues items that haven't transitioned + * to a completed state, or that failed + */ + +export type DBInsert = { + [K in keyof DB]: Insertable +} + +const POLLING_PERIOD_SEC = 60 // How often do we try to grab the lock and re-index? +const Q_MAX_LENGTH = 100 // How many EmbeddingIndex items do we batch in redis? +const WORD_COUNT_TO_TOKEN_RATIO = 3.0 / 2 // We multiple the word count by this to estimate token count + +const {AI_EMBEDDER_ENABLED} = process.env +const {SERVER_ID} = process.env + +tracer.init({ + service: `embedder`, + appsec: process.env.DD_APPSEC_ENABLED === 'true', + plugins: false, + version: process.env.npm_package_version +}) +tracer.use('pg') + +const refreshMetadata = async () => { + const dataLoader = getRootDataLoader() + await refreshRetroDiscussionTopicsMeta(dataLoader) + // In the future, other sorts of objects to index could be added here... +} +const maybeQueueMetadataItems = async (modelManager: ModelManager) => { + const redisClient = getRedisClient() + const queueLength = await redisClient.zcard('embedder:queue') + if (queueLength >= Q_MAX_LENGTH) return + const itemCountToQueue = Q_MAX_LENGTH - queueLength + const modelTables = modelManager.embeddingModels.map((m) => m.tableName) + const orgIds = await orgIdsWithFeatureFlag() + + // For each configured embedding model, select rows from EmbeddingsMetadata + // that haven't been calculated nor exist in the EmbeddingsJobQueue yet + // + // Notes: + // * `em.models @> ARRAY[v.model]` is an indexed query + // * I don't love all overrides, I wish there was a better way + // see: https://github.com/kysely-org/kysely/issues/872 + + const batchToQueue = await selectMetaToQueue(modelTables, orgIds, itemCountToQueue) + + if (!batchToQueue.length) { + console.log(`embedder: no new items to queue`) + return + } + + const ejqHash: { + [key: string]: { + refUpdatedAt: Date + } + } = {} + const makeKey = (item: {objectType: string; refId: string}) => `${item.objectType}:${item.refId}` + + const ejqValues = batchToQueue.map((item) => { + ejqHash[makeKey(item)] = { + refUpdatedAt: item.refUpdatedAt + } + return { + objectType: item.objectType, + refId: item.refId as string, + model: item.model, + state: 'queued' as const + } + }) + + const ejqRows = await insertNewJobs(ejqValues) + + ejqRows.forEach((item) => { + const {refUpdatedAt} = ejqHash[makeKey(item)]! + const score = new Date(refUpdatedAt).getTime() + redisClient.zadd('embedder:queue', score, item.id) + }) + + console.log(`embedder: queued ${batchToQueue.length} items`) +} + +const dequeueAndEmbedUntilEmpty = async (modelManager: ModelManager) => { + const dataLoader = getRootDataLoader() + const redisClient = getRedisClient() + while (true) { + const maybeRedisQItem = await redisClient.zpopmax('embedder:queue', 1) + if (maybeRedisQItem.length < 2) return // Q is empty, all done! + + const [id, _] = maybeRedisQItem + if (!id) { + console.log(`embedder: de-queued undefined item from embedder:queue`) + continue + } + const jobQueueId = parseInt(id, 10) + const jobQueueItem = await selectJobQueueItemById(jobQueueId) + if (!jobQueueItem) { + console.log(`embedder: unable to fetch EmbeddingsJobQueue.id = ${id}`) + continue + } + + const metadata = await selectMetadataByJobQueueId(jobQueueId) + if (!metadata) { + await updateJobState(jobQueueId, 'failed', { + stateMessage: `unable to fetch metadata by EmbeddingsJobQueue.id = ${id}` + }) + continue + } + + let fullText = metadata?.fullText + try { + if (!fullText) { + fullText = await createEmbeddingTextFrom(jobQueueItem, dataLoader) + } + } catch (e) { + await updateJobState(jobQueueId, 'failed', { + stateMessage: `unable to create embedding text: ${e}` + }) + continue + } + + const wordCount = countWords(fullText) + + const embeddingModel = modelManager.embeddingModelsMapByTable[jobQueueItem.model] + if (!embeddingModel) { + await updateJobState(jobQueueId, 'failed', { + stateMessage: `embedding model ${jobQueueItem.model} not available` + }) + continue + } + const itemKey = `${jobQueueItem.objectType}:${jobQueueItem.refId}` + const modelTable = embeddingModel.tableName + + let embedText = fullText + const maxInputTokens = embeddingModel.maxInputTokens + // we're using word count as an appoximation of tokens + if (wordCount * WORD_COUNT_TO_TOKEN_RATIO > maxInputTokens) { + try { + const generator = modelManager.generationModels[0] // use 1st generator + if (!generator) throw new Error(`Generator unavailable`) + const summarizeOptions = {maxInputTokens, truncate: true} + console.log(`embedder: ...summarizing ${itemKey} for ${modelTable}`) + embedText = await generator.summarize(fullText, summarizeOptions) + } catch (e) { + await updateJobState(jobQueueId, 'failed', { + stateMessage: `unable to summarize long embed text: ${e}` + }) + continue + } + } + // console.log(`embedText: ${embedText}`) + + let embeddingVector: number[] + try { + embeddingVector = await embeddingModel.getEmbedding(embedText) + } catch (e) { + await updateJobState(jobQueueId, 'failed', { + stateMessage: `unable to get embeddings: ${e}` + }) + continue + } + + // complete job, do the following atomically + // (1) update EmbeddingsMetadata to reflect model completion + // (2) upsert model table row with embedding + // (3) delete EmbeddingsJobQueue row + await completeJobTxn(modelTable, jobQueueId, metadata, fullText, embedText, embeddingVector) + console.log(`embedder: completed ${itemKey} -> ${modelTable}`) + } +} + +const tick = async (modelManager: ModelManager) => { + console.log(`embedder: tick`) + const redisClient = getRedisClient() + const redlock = new Redlock([redisClient], { + driftFactor: 0.01, + retryCount: 10, + retryDelay: 250, + retryJitter: 50, + automaticExtensionThreshold: 500 + }) + + await redlock + .using(['embedder:lock'], 10000, async (signal: RedlockAbortSignal) => { + console.log(`embedder: acquired index queue lock`) + // N.B. one of the many benefits of using redlock is the using() interface + // will automatically extend the lock if these operations exceed the + // original redis timeout time + await refreshMetadata() + await maybeQueueMetadataItems(modelManager) + + if (signal.aborted) { + // Not certain which conditions this would happen, it would + // happen after operations took place, so nothing much to do here. + console.log('embedder: lock was lost!') + } + }) + .catch((err: string) => { + // Handle errors (including lock acquisition errors) + console.error('embedder: an error occurred ', err) + }) + console.log('embedder: index queue lock released') + + // get the highest priority item and embed it + await dequeueAndEmbedUntilEmpty(modelManager) + + setTimeout(() => tick(modelManager), POLLING_PERIOD_SEC * 1000) +} + +function parseEnvBoolean(envVarValue: string | undefined): boolean { + return envVarValue === 'true' +} + +const run = async () => { + console.log(`embedder: run()`) + const embedderEnabled = parseEnvBoolean(AI_EMBEDDER_ENABLED) + const modelManager = getModelManager() + if (embedderEnabled && modelManager) { + const pg = getKysely() + await modelManager.maybeCreateTables(pg) + console.log(`\n⚡⚡⚡️️ Server ID: ${SERVER_ID}. Embedder is ready ⚡⚡⚡️️️`) + tick(modelManager) + } else { + console.log(`embedder: no valid configuration (check AI_EMBEDDER_ENABLED in .env)`) + // exit + } +} + +run() diff --git a/packages/embedder/indexing/countWords.ts b/packages/embedder/indexing/countWords.ts new file mode 100644 index 00000000000..75dae3effa2 --- /dev/null +++ b/packages/embedder/indexing/countWords.ts @@ -0,0 +1,17 @@ +export function countWords(text: string) { + let count = 0 + let inWord = false + + for (const char of text) { + if (/\w/.test(char)) { + if (!inWord) { + count++ + inWord = true + } + } else { + inWord = false + } + } + + return count +} diff --git a/packages/embedder/indexing/createEmbeddingTextFrom.ts b/packages/embedder/indexing/createEmbeddingTextFrom.ts new file mode 100644 index 00000000000..9d6e66b60e7 --- /dev/null +++ b/packages/embedder/indexing/createEmbeddingTextFrom.ts @@ -0,0 +1,15 @@ +import {Selectable} from 'kysely' +import {DB} from 'parabol-server/postgres/pg' +import {DataLoaderWorker} from 'parabol-server/graphql/graphql' + +import {createText as createTextFromRetrospectiveDiscussionTopic} from './retrospectiveDiscussionTopic' + +export const createEmbeddingTextFrom = async ( + item: Selectable, + dataLoader: DataLoaderWorker +): Promise => { + switch (item.objectType) { + case 'retrospectiveDiscussionTopic': + return createTextFromRetrospectiveDiscussionTopic(item, dataLoader) + } +} diff --git a/packages/embedder/indexing/embeddingsTablesOps.ts b/packages/embedder/indexing/embeddingsTablesOps.ts new file mode 100644 index 00000000000..b68bc21ccbe --- /dev/null +++ b/packages/embedder/indexing/embeddingsTablesOps.ts @@ -0,0 +1,198 @@ +import {Insertable, Selectable, Updateable, sql} from 'kysely' +import getKysely from 'parabol-server/postgres/getKysely' +import {DB} from 'parabol-server/postgres/pg' +import {DBInsert} from '../embedder' +import {RawBuilder} from 'kysely' +import numberVectorToString from './numberVectorToString' + +function unnestedArray(maybeArray: T[] | T): RawBuilder { + let a: T[] = Array.isArray(maybeArray) ? maybeArray : [maybeArray] + return sql`unnest(ARRAY[${sql.join(a)}]::varchar[])` +} + +export const selectJobQueueItemById = async ( + id: number +): Promise | undefined> => { + const pg = getKysely() + return pg.selectFrom('EmbeddingsJobQueue').selectAll().where('id', '=', id).executeTakeFirst() +} +export const selectMetadataByJobQueueId = async ( + id: number +): Promise | undefined> => { + const pg = getKysely() + return pg + .selectFrom('EmbeddingsMetadata as em') + .selectAll() + .leftJoin('EmbeddingsJobQueue as ejq', (join) => + join.onRef('em.objectType', '=', 'ejq.objectType').onRef('em.refId', '=', 'ejq.refId') + ) + .where('ejq.id', '=', id) + .executeTakeFirstOrThrow() +} + +// For each configured embedding model, select rows from EmbeddingsMetadata +// that haven't been calculated nor exist in the EmbeddingsJobQueue yet +// +// Notes: +// * `em.models @> ARRAY[v.model]` is an indexed query +// * I don't love all overrides, I wish there was a better way +// see: https://github.com/kysely-org/kysely/issues/872 +export async function selectMetaToQueue( + configuredModels: string[], + orgIds: any[], + itemCountToQueue: number +) { + const pg = getKysely() + const maybeMetaToQueue = (await pg + .selectFrom('EmbeddingsMetadata as em') + .selectAll('em') + .leftJoinLateral(unnestedArray(configuredModels).as('model'), (join) => join.onTrue()) + .leftJoin('Team as t', 'em.teamId', 't.id') + .select('model' as any) + .where(({eb, not, or, and, exists, selectFrom}) => + and([ + or([ + not(eb('em.models', '<@', sql`ARRAY[${sql.ref('model')}]::varchar[]` as any) as any), + eb('em.models' as any, 'is', null) + ]), + not( + exists( + selectFrom('EmbeddingsJobQueue as ejq') + .select('ejq.id') + .whereRef('em.objectType', '=', 'ejq.objectType') + .whereRef('em.refId', '=', 'ejq.refId') + .whereRef('ejq.model', '=', 'model' as any) + ) + ), + eb('t.orgId', 'in', orgIds) + ]) + ) + .limit(itemCountToQueue) + .execute()) as unknown as Selectable[] + + type MetadataToQueue = Selectable< + Omit & { + refId: NonNullable + } & {model: string} + > + + return maybeMetaToQueue.filter( + (item) => item.refId !== null && item.refId !== undefined + ) as MetadataToQueue[] +} + +export const updateJobState = async ( + id: number, + state: Updateable['state'], + jobQueueFields: Updateable = {} +) => { + const pg = getKysely() + const jobQueueColumns: Updateable = { + ...jobQueueFields, + state + } + if (state === 'failed') console.log(`embedder: failed job ${id}, ${jobQueueFields.stateMessage}`) + return pg + .updateTable('EmbeddingsJobQueue') + .set(jobQueueColumns) + .where('id', '=', id) + .executeTakeFirstOrThrow() +} + +export function insertNewJobs(ejqValues: Insertable[]) { + const pg = getKysely() + return pg + .insertInto('EmbeddingsJobQueue') + .values(ejqValues) + .returning(['id', 'objectType', 'refId']) + .execute() +} + +// complete job, do the following atomically +// (1) update EmbeddingsMetadata to reflect model completion +// (2) upsert model table row with embedding +// (3) delete EmbeddingsJobQueue row +export function completeJobTxn( + modelTable: string, + jobQueueId: number, + metadata: Updateable, + fullText: string, + embedText: string, + embeddingVector: number[] +) { + const pg = getKysely() + return pg.transaction().execute(async (trx) => { + // get fields to update correct metadata row + const jobQueueItem = await trx + .selectFrom('EmbeddingsJobQueue') + .select(['objectType', 'refId', 'model']) + .where('id', '=', jobQueueId) + .executeTakeFirstOrThrow() + + // (1) update metadata row + const metadataColumnsToUpdate: { + models: RawBuilder + fullText?: string | null | undefined + } = { + // update models as a set + models: sql`( +SELECT array_agg(DISTINCT value) +FROM ( + SELECT unnest(COALESCE("models", '{}')) AS value + UNION + SELECT unnest(ARRAY[${modelTable}]::VARCHAR[]) AS value +) AS combined_values +)` + } + + if (metadata?.fullText !== fullText) { + metadataColumnsToUpdate.fullText = fullText + } + + const updatedMetadata = await trx + .updateTable('EmbeddingsMetadata') + .set(metadataColumnsToUpdate) + .where('objectType', '=', jobQueueItem.objectType) + .where('refId', '=', jobQueueItem.refId) + .returning(['id']) + .executeTakeFirstOrThrow() + + // (2) upsert into model table + await trx + .insertInto(modelTable as any) + .values({ + embedText: fullText !== embedText ? embedText : null, + embedding: numberVectorToString(embeddingVector), + embeddingsMetadataId: updatedMetadata.id + }) + .onConflict((oc) => + oc.column('id').doUpdateSet((eb) => ({ + embedText: eb.ref('excluded.embedText'), + embeddingsMetadataId: eb.ref('excluded.embeddingsMetadataId') + })) + ) + .executeTakeFirstOrThrow() + + // (3) delete completed job queue item + return await trx + .deleteFrom('EmbeddingsJobQueue') + .where('id', '=', jobQueueId) + .executeTakeFirstOrThrow() + }) +} +export async function upsertEmbeddingsMetaRows( + embeddingsMetaRows: DBInsert['EmbeddingsMetadata'][] +) { + const pg = getKysely() + return pg + .insertInto('EmbeddingsMetadata') + .values(embeddingsMetaRows) + .onConflict((oc) => + oc.columns(['objectType', 'refId']).doUpdateSet((eb) => ({ + objectType: eb.ref('excluded.objectType'), + refId: eb.ref('excluded.refId'), + refUpdatedAt: eb.ref('excluded.refUpdatedAt') + })) + ) + .execute() +} diff --git a/packages/embedder/indexing/getRedisClient.ts b/packages/embedder/indexing/getRedisClient.ts new file mode 100644 index 00000000000..7aaf65be33c --- /dev/null +++ b/packages/embedder/indexing/getRedisClient.ts @@ -0,0 +1,11 @@ +import RedisInstance from 'parabol-server/utils/RedisInstance' + +const {SERVER_ID} = process.env + +let redisClient: RedisInstance +export const getRedisClient = () => { + if (!redisClient) { + redisClient = new RedisInstance(`embedder-${SERVER_ID}`) + } + return redisClient +} diff --git a/packages/embedder/indexing/getRootDataLoader.ts b/packages/embedder/indexing/getRootDataLoader.ts new file mode 100644 index 00000000000..304c0c01058 --- /dev/null +++ b/packages/embedder/indexing/getRootDataLoader.ts @@ -0,0 +1,10 @@ +import getDataLoader from 'parabol-server/graphql/getDataLoader' +import {DataLoaderWorker} from 'parabol-server/graphql/graphql' + +let rootDataLoader: DataLoaderWorker +export const getRootDataLoader = () => { + if (!rootDataLoader) { + rootDataLoader = getDataLoader() as DataLoaderWorker + } + return rootDataLoader +} diff --git a/packages/embedder/indexing/numberVectorToString.ts b/packages/embedder/indexing/numberVectorToString.ts new file mode 100644 index 00000000000..df49a716d57 --- /dev/null +++ b/packages/embedder/indexing/numberVectorToString.ts @@ -0,0 +1,5 @@ +function numberVectorToString(vector: number[]): string { + return '[' + vector.join(', ') + ']' +} + +export default numberVectorToString diff --git a/packages/embedder/indexing/orgIdsWithFeatureFlag.ts b/packages/embedder/indexing/orgIdsWithFeatureFlag.ts new file mode 100644 index 00000000000..82d86702e67 --- /dev/null +++ b/packages/embedder/indexing/orgIdsWithFeatureFlag.ts @@ -0,0 +1,15 @@ +import getRethink from 'parabol-server/database/rethinkDriver' +import {RDatum} from 'parabol-server/database/stricterR' + +export const orgIdsWithFeatureFlag = async () => { + // I had to add a secondary index to the Organization table to get + // this query to be cheap + const r = await getRethink() + return await r + .table('Organization') + .getAll('relatedDiscussions', {index: 'featureFlagsIndex' as any}) + .filter((r: RDatum) => r('featureFlags').contains('relatedDiscussions')) + .map((r: RDatum) => r('id')) + .coerceTo('array') + .run() +} diff --git a/packages/embedder/indexing/retrospectiveDiscussionTopic.ts b/packages/embedder/indexing/retrospectiveDiscussionTopic.ts new file mode 100644 index 00000000000..f31aab74cc2 --- /dev/null +++ b/packages/embedder/indexing/retrospectiveDiscussionTopic.ts @@ -0,0 +1,326 @@ +import {Selectable} from 'kysely' +import prettier from 'prettier' + +import getRethink, {RethinkSchema} from 'parabol-server/database/rethinkDriver' +import {DataLoaderWorker} from 'parabol-server/graphql/graphql' +import getKysely from 'parabol-server/postgres/getKysely' +import {DB} from 'parabol-server/postgres/pg' + +import Comment from 'parabol-server/database/types/Comment' +import DiscussStage from 'parabol-server/database/types/DiscussStage' +import MeetingRetrospective, { + isMeetingRetrospective +} from 'parabol-server/database/types/MeetingRetrospective' + +import {upsertEmbeddingsMetaRows} from './embeddingsTablesOps' +import {AnyMeeting} from 'parabol-server/postgres/types/Meeting' + +const BATCH_SIZE = 1000 + +export interface EmbeddingsJobQueueRetrospectiveDiscussionTopic + extends Omit { + objectType: 'retrospectiveDiscussionTopic' +} + +// Here's a generic reprentation of the text generated here: + +// A topic "" was discussed during the meeting "" +// that followed the "" template. +// +// +// Participants were prompted with, ": ". +// - wrote, "" +// +// +// +// +// A discussion was held. +// + +const IGNORE_COMMENT_USER_IDS = ['parabolAIUser'] + +const pg = getKysely() + +export async function refreshRetroDiscussionTopicsMeta(dataLoader: DataLoaderWorker) { + const r = await getRethink() + const {createdAt: newestMeetingDate} = (await r + .table('NewMeeting') + .max({index: 'createdAt'}) + .run()) as unknown as RethinkSchema['NewMeeting']['type'] + const {createdAt: oldestMeetingDate} = (await r + .table('NewMeeting') + .min({index: 'createdAt'}) + .run()) as unknown as RethinkSchema['NewMeeting']['type'] + + const {newestMetaDate} = (await pg + .selectFrom('EmbeddingsMetadata') + .select(pg.fn.max('refUpdatedAt').as('newestMetaDate')) + .where('objectType', '=', 'retrospectiveDiscussionTopic') + .executeTakeFirst()) ?? {newestMetaDate: null} + let startDateTime = newestMetaDate || oldestMeetingDate + + if (startDateTime.getTime() === newestMeetingDate.getTime()) return + + console.log( + `refreshRetroDiscussionTopicsMeta(): ` + + `will consider adding items from ${startDateTime.toISOString()} to ` + + `${newestMeetingDate.toISOString()}` + ) + + let totalAdded = 0 + do { + // Process history in batches. + // + // N.B. We add historical meetings to the EmbeddingsMetadata table here. + // This query will intentionally miss meetings that haven't been completed + // (`summarySentAt` is null). These meetings will need to be added to the + // EmbeddingsMetadata table by a hook that runs when the meetings complete. + const {maxCreatedAt, completedNewMeetings} = await r + .table('NewMeeting') + .between(startDateTime, newestMeetingDate, {rightBound: 'closed', index: 'createdAt'}) + .orderBy({index: 'createdAt'}) + .limit(BATCH_SIZE) + .coerceTo('array') + .do((rows: any) => ({ + maxCreatedAt: r.expr(rows).max('createdAt')('createdAt'), // Then find the max createdAt value + completedNewMeetings: r.expr(rows).filter((r: any) => + r('meetingType') + .eq('retrospective') + .and( + r('endedAt').gt(0), + r + .hasFields('phases') + .and(r('phases').count().gt(0)) + .and( + r('phases') + .filter((phase: any) => phase('phaseType').eq('discuss')) + .filter((phase: any) => + phase.hasFields('stages').and(phase('stages').count().gt(0)) + ) + .count() + .gt(0) + ) + ) + ) + })) + .run() + const embeddingsMetaRows = ( + await Promise.all( + completedNewMeetings.map((m: AnyMeeting) => + newRetroDiscussionTopicsFromNewMeeting(m, dataLoader) + ) + ) + ).flat() + if (embeddingsMetaRows.length > 0) { + await upsertEmbeddingsMetaRows(embeddingsMetaRows) + totalAdded += embeddingsMetaRows.length + console.log( + `refreshRetroDiscussionTopicsMeta(): synced to ${maxCreatedAt.toISOString()}, added` + + ` ${embeddingsMetaRows.length} retrospectiveDiscussionTopics` + ) + } + + // N.B. In the unlikely event that we have >=BATCH_SIZE meetings that end at _exactly_ + // the same timetsamp, this will loop forever. + if ( + startDateTime.getTime() === newestMeetingDate.getTime() && + completedNewMeetings.length < BATCH_SIZE + ) + break + startDateTime = maxCreatedAt + } while (true) + + console.log( + `refreshRetroDiscussionTopicsMeta(): added ${totalAdded} total retrospectiveDiscussionTopics` + ) +} + +async function getPreferredNameByUserId(userId: string, dataLoader: DataLoaderWorker) { + const user = await dataLoader.get('users').load(userId) + return !user ? 'Unknown' : user.preferredName +} + +async function formatThread( + dataLoader: DataLoaderWorker, + comments: Comment[], + parentId: string | null = null, + depth = 0 +): Promise { + // Filter and sort comments as before + const filteredComments = comments + .filter((comment) => comment.threadParentId === parentId) + .sort((a, b) => (a.threadSortOrder < b.threadSortOrder ? -1 : 1)) + + // Use map to create an array of promises for each formatted comment string + const formattedCommentsPromises = filteredComments.map(async (comment) => { + const indent = ' '.repeat(depth + 1) + const author = comment.isAnonymous + ? 'Anonymous' + : comment.createdBy + ? await getPreferredNameByUserId(comment.createdBy, dataLoader) + : 'Unknown' + const how = depth === 0 ? 'wrote' : 'replied' + const content = comment.plaintextContent + const formattedPost = `${indent}- ${author} ${how}, "${content}"\n` + + // Recursively format child threads + const childThread = await formatThread(dataLoader, comments, comment.id, depth + 1) + return formattedPost + '\n' + childThread + }) + + // Resolve all promises and join the results + const formattedComments = await Promise.all(formattedCommentsPromises) + return formattedComments.join('') +} + +export const createTextFromNewMeetingDiscussionStage = async ( + newMeeting: MeetingRetrospective, + stageId: string, + dataLoader: DataLoaderWorker, + textForReranking: boolean = false +) => { + if (!newMeeting) throw 'newMeeting is undefined' + if (!isMeetingRetrospective(newMeeting)) throw 'newMeeting is not retrospective' + if (!newMeeting.templateId) throw 'template is undefined' + const template = await dataLoader.get('meetingTemplates').load(newMeeting.templateId) + if (!template) throw 'template is undefined' + const discussPhase = newMeeting.phases.find((phase) => phase.phaseType === 'discuss') + if (!discussPhase) throw 'newMeeting discuss phase is undefined' + if (!discussPhase.stages) throw 'newMeeting discuss phase has no stages' + const discussStage = discussPhase.stages.find((stage) => stage.id === stageId) as DiscussStage + if (!discussStage) throw 'newMeeting discuss stage not found' + const {summary: discussionSummary} = discussStage.discussionId + ? (await dataLoader.get('discussions').load(discussStage.discussionId)) ?? {summary: null} + : {summary: null} + const r = await getRethink() + if (!discussStage.reflectionGroupId) throw 'newMeeting discuss stage has no reflectionGroupId' + const reflectionGroup = await r + .table('RetroReflectionGroup') + .get(discussStage.reflectionGroupId) + .run() + if (!reflectionGroup.id) throw 'newMeeting reflectionGroup has no id' + const reflections = await r + .table('RetroReflection') + .getAll(reflectionGroup.id, {index: 'reflectionGroupId'}) + .run() + const promptIds = [...new Set(reflections.map((r) => r.promptId))] + let markdown = '' + if (!textForReranking) + markdown = + `A topic "${reflectionGroup.title}" was discussed during ` + + `the meeting "${newMeeting.name}" that followed the "${template.name}" template.\n` + + `\n` + const prompts = await dataLoader.get('reflectPrompts').loadMany(promptIds) + for (const prompt of prompts) { + if (!prompt || prompt instanceof Error) continue + if (!textForReranking) { + markdown += `Participants were prompted with, "${prompt.question}` + if (prompt.description) markdown += `: ${prompt.description}` + markdown += `".\n` + } + if (newMeeting.disableAnonymity) { + for (const reflection of reflections.filter((r) => r.promptId === prompt.id)) { + const author = await getPreferredNameByUserId(reflection.creatorId, dataLoader) + markdown += ` - ${author} wrote, "${reflection.plaintextContent}"\n` + } + } else { + for (const reflection of reflections.filter((r) => r.promptId === prompt.id)) { + markdown += ` - Anonymous wrote, "${reflection.plaintextContent}"\n` + } + } + markdown += `\n` + } + + markdown += `\n` + + /** + * The choice I made here was default to the summary of the discussion if it exists in order to make this textual + * representation of a retrospective discussion a shorter token count. Using the summary almost certainly ensures + * this text won't need to be sent to be summarized again before an embedding vector is calculated. + * + * If we included the comments all the time, then we're maximizing the chance that rarer tokens might end up in the + * embed text and these tokens might affect the final embed vector in a useful way. However, we increase the odds that + * the embed text will need to be summarized before the vector is calculated. + * + * I decided to "just be cheap" and try and minimize calls to the summarizer. + * + * If we wanted to compare and contrast these approaches, we could always generate a second set of embed vectors + * objectType: 'retrospectiveDiscussionNoSummary' or something and do a bit of testing. + */ + + if (discussionSummary) { + markdown += `Further discussion was made. ` + ` ${discussionSummary}` + } else { + const comments = await dataLoader.get('commentsByDiscussionId').load(stageId) + + const sortedComments = comments + .map((comment) => { + if (!comment.threadParentId) { + return { + ...comment, + threadParentId: null + } + } + return comment + }) + .sort((a, b) => { + if (a.threadParentId === b.threadParentId) { + return a.threadSortOrder - b.threadSortOrder + } + if (a.threadParentId == null) return 1 + if (b.threadParentId == null) return -1 + return a.threadParentId > b.threadParentId ? 1 : -1 + }) as Comment[] + + const filteredComments = sortedComments.filter( + (c) => !IGNORE_COMMENT_USER_IDS.includes(c.createdBy) + ) + if (filteredComments.length) { + markdown += `Futher discussion was made:\n` + markdown += await formatThread(dataLoader, filteredComments) + // TODO: if the discussion threads are too long, summarize them + } + } + + markdown = prettier.format(markdown, { + parser: 'markdown', + proseWrap: 'always', + printWidth: 72 + }) + + return markdown +} + +export const createText = async ( + item: Selectable, + dataLoader: DataLoaderWorker +): Promise => { + if (!item.refId) throw 'refId is undefined' + const [newMeetingId, discussionId] = item.refId.split(':') + if (!newMeetingId) throw new Error('newMeetingId cannot be undefined') + if (!discussionId) throw new Error('discussionId cannot be undefined') + const newMeeting = await dataLoader.get('newMeetings').load(newMeetingId) + return createTextFromNewMeetingDiscussionStage( + newMeeting as MeetingRetrospective, + discussionId, + dataLoader + ) +} + +export const newRetroDiscussionTopicsFromNewMeeting = async ( + newMeeting: RethinkSchema['NewMeeting']['type'], + dataLoader: DataLoaderWorker +) => { + const discussPhase = newMeeting.phases.find((phase) => phase.phaseType === 'discuss') + const orgId = (await dataLoader.get('teams').load(newMeeting.teamId))?.orgId + if (orgId && discussPhase && discussPhase.stages) { + return discussPhase.stages.map((stage) => ({ + objectType: 'retrospectiveDiscussionTopic' as const, + teamId: newMeeting.teamId, + refId: `${newMeeting.id}:${stage.id}`, + refUpdatedAt: newMeeting.createdAt + })) + } else { + return [] + } +} diff --git a/packages/embedder/package.json b/packages/embedder/package.json new file mode 100644 index 00000000000..47af8737dac --- /dev/null +++ b/packages/embedder/package.json @@ -0,0 +1,31 @@ +{ + "name": "parabol-embedder", + "version": "7.10.0", + "description": "A service that computes embedding vectors from Parabol objects", + "author": "Jordan Husney ", + "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/embedder#readme", + "license": "AGPL-3.0-or-later", + "repository": { + "type": "git", + "url": "git+https://github.com/ParabolInc/parabol.git" + }, + "scripts": { + "typecheck": "yarn tsc --noEmit -p tsconfig.json" + }, + "bugs": { + "url": "https://github.com/ParabolInc/parabol/issues" + }, + "devDependencies": { + "@babel/cli": "7.18.6", + "@babel/core": "7.18.6", + "@types/node": "^16.11.62", + "babel-plugin-inline-import": "^3.0.0", + "sucrase": "^3.32.0", + "ts-node-dev": "^1.0.0-pre.44", + "typescript": "4.9.5" + }, + "dependencies": { + "dd-trace": "^4.2.0", + "redlock": "^5.0.0-beta.2" + } +} diff --git a/packages/embedder/tsconfig.json b/packages/embedder/tsconfig.json new file mode 100644 index 00000000000..1d179d08096 --- /dev/null +++ b/packages/embedder/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "../", + "paths": { + // when we import from lib, make goto-definition point to the src + "parabol-server/*": ["server/*"], + "parabol-client/*": ["client/*"] + }, + "outDir": "lib", + "lib": ["esnext"], + "types": ["node"] + }, + "files": ["../server/types/modules.d.ts"] +} diff --git a/packages/gql-executor/package.json b/packages/gql-executor/package.json index 1675bcaf5ea..84dbff4bdfb 100644 --- a/packages/gql-executor/package.json +++ b/packages/gql-executor/package.json @@ -1,6 +1,6 @@ { "name": "gql-executor", - "version": "7.17.0", + "version": "7.19.3", "description": "A Stateless GraphQL Executor", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/gqlExecutor#readme", @@ -27,8 +27,8 @@ }, "dependencies": { "dd-trace": "^4.2.0", - "parabol-client": "7.17.0", - "parabol-server": "7.17.0", + "parabol-client": "7.19.3", + "parabol-server": "7.19.3", "undici": "^5.26.2" } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 0190f526ea8..f105f56d7da 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -2,7 +2,7 @@ "name": "integration-tests", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.17.0", + "version": "7.19.3", "description": "", "main": "index.js", "scripts": { diff --git a/packages/server/billing/helpers/adjustUserCount.ts b/packages/server/billing/helpers/adjustUserCount.ts index b7cf07df1a5..15c440c2834 100644 --- a/packages/server/billing/helpers/adjustUserCount.ts +++ b/packages/server/billing/helpers/adjustUserCount.ts @@ -14,6 +14,7 @@ import handleEnterpriseOrgQuantityChanges from './handleEnterpriseOrgQuantityCha import handleTeamOrgQuantityChanges from './handleTeamOrgQuantityChanges' import {getUserById} from '../../postgres/queries/getUsersByIds' import {DataLoaderWorker} from '../../graphql/graphql' +import {Logger} from '../../utils/Logger' const maybeUpdateOrganizationActiveDomain = async ( orgId: string, @@ -170,5 +171,5 @@ export default async function adjustUserCount( .run() handleEnterpriseOrgQuantityChanges(paidOrgs, dataLoader).catch() - handleTeamOrgQuantityChanges(paidOrgs).catch(console.error) + handleTeamOrgQuantityChanges(paidOrgs).catch(Logger.error) } diff --git a/packages/server/billing/helpers/terminateSubscription.ts b/packages/server/billing/helpers/terminateSubscription.ts index b5fdd8a8070..0b1c6d3f4c5 100644 --- a/packages/server/billing/helpers/terminateSubscription.ts +++ b/packages/server/billing/helpers/terminateSubscription.ts @@ -1,5 +1,6 @@ import getRethink from '../../database/rethinkDriver' import Organization from '../../database/types/Organization' +import {Logger} from '../../utils/Logger' import {getStripeManager} from '../../utils/stripe' const terminateSubscription = async (orgId: string) => { @@ -30,7 +31,7 @@ const terminateSubscription = async (orgId: string) => { try { await manager.deleteSubscription(stripeSubscriptionId) } catch (e) { - console.error(`cannot delete subscription ${stripeSubscriptionId}`, e) + Logger.error(`cannot delete subscription ${stripeSubscriptionId}`, e) } } return stripeSubscriptionId diff --git a/packages/server/dataloader/azureDevOpsLoaders.ts b/packages/server/dataloader/azureDevOpsLoaders.ts index 2c9a2b4617f..7892bce6f13 100644 --- a/packages/server/dataloader/azureDevOpsLoaders.ts +++ b/packages/server/dataloader/azureDevOpsLoaders.ts @@ -13,6 +13,7 @@ import AzureDevOpsServerManager, { TeamProjectReference, WorkItem } from '../utils/AzureDevOpsServerManager' +import {Logger} from '../utils/Logger' import sendToSentry from '../utils/sendToSentry' import RootDataLoader from './RootDataLoader' @@ -215,7 +216,7 @@ export const azureDevOpsAllWorkItems = ( const {error, workItems} = restResult if (error !== undefined || workItems === undefined) { - console.log(error) + Logger.log(error) return [] as AzureDevOpsWorkItem[] } @@ -264,7 +265,7 @@ export const azureDevUserInfo = ( const restResult = await manager.getMe() const {error, azureDevOpsUser} = restResult if (error !== undefined || azureDevOpsUser === undefined) { - console.log(error) + Logger.log(error) return undefined } return { @@ -303,7 +304,7 @@ export const allAzureDevOpsAccessibleOrgs = ( const results = await manager.getAccessibleOrgs(id) const {error, accessibleOrgs} = results // handle error if defined - console.log(error) + Logger.log(error) return accessibleOrgs.map((resource) => ({ ...resource })) @@ -340,7 +341,7 @@ export const allAzureDevOpsProjects = ( ) const {error, projects} = await manager.getAllUserProjects() if (error !== undefined) { - console.log(error) + Logger.log(error) return [] } if (projects !== null) resultReferences.push(...projects) @@ -383,7 +384,7 @@ export const azureDevOpsProject = ( ) const projectRes = await manager.getProject(instanceId, projectId) if (projectRes instanceof Error) { - console.log(projectRes) + Logger.log(projectRes) return null } return { @@ -475,7 +476,7 @@ export const azureDevOpsUserStory = ( const restResult = await manager.getWorkItemData(instanceId, workItemIds) const {error, workItems} = restResult if (error !== undefined || workItems.length !== 1 || !workItems[0]) { - console.log(error) + Logger.log(error) return null } else { const returnedWorkItem: WorkItem = workItems[0] @@ -637,7 +638,7 @@ export const azureDevOpsWorkItems = ( const workItemData = await manager.getWorkItemData(instanceId, workItemIds) const {error: workItemDataError, workItems: returnedWorkItems} = workItemData if (workItemDataError !== undefined) { - console.log(error) + Logger.log(error) return [] } diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index b39e1e42d30..8bd64c6250b 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -1,5 +1,5 @@ import DataLoader from 'dataloader' -import {Selectable, sql} from 'kysely' +import {Selectable, SqlBool, sql} from 'kysely' import {PARABOL_AI_USER_ID} from '../../client/utils/constants' import getRethink, {RethinkSchema} from '../database/rethinkDriver' import {RDatum} from '../database/stricterR' @@ -478,11 +478,11 @@ export const meetingTemplatesByOrgId = (parent: RootDataLoader) => { .selectAll() .where('orgId', 'in', orgIds) .where('isActive', '=', true) - .where(({or, cmpr}) => + .where(({or, eb}) => or([ - cmpr('hideStartingAt', 'is', null), - sql`make_date(2020 , extract(month from current_date)::integer, extract(day from current_date)::integer) between "hideEndingAt" and "hideStartingAt"`, - sql`make_date(2019 , extract(month from current_date)::integer, extract(day from current_date)::integer) between "hideEndingAt" and "hideStartingAt"` + eb('hideStartingAt', 'is', null), + sql`make_date(2020 , extract(month from current_date)::integer, extract(day from current_date)::integer) between "hideEndingAt" and "hideStartingAt"`, + sql`make_date(2019 , extract(month from current_date)::integer, extract(day from current_date)::integer) between "hideEndingAt" and "hideStartingAt"` ]) ) .orderBy('createdAt', 'desc') diff --git a/packages/server/dataloader/customRedisQueries.ts b/packages/server/dataloader/customRedisQueries.ts index b3ed754dcab..a461202ca5a 100644 --- a/packages/server/dataloader/customRedisQueries.ts +++ b/packages/server/dataloader/customRedisQueries.ts @@ -1,7 +1,7 @@ // Sometimes, a value cached is redis is harder to get than simply querying the primary key on a table // this allows redis to cache the results of arbitrarily complex rethinkdb queries -import {sql} from 'kysely' +import {sql, SqlBool} from 'kysely' import ms from 'ms' import getRethink from '../database/rethinkDriver' import {RDatum} from '../database/stricterR' @@ -38,11 +38,11 @@ const customRedisQueries = { .where('teamId', '=', 'aGhostTeam') .where('isActive', '=', true) .where('type', '=', templateType) - .where(({or, cmpr}) => + .where(({or, eb}) => or([ - cmpr('hideStartingAt', 'is', null), - sql`make_date(2020 , extract(month from current_date)::integer, extract(day from current_date)::integer) between "hideEndingAt" and "hideStartingAt"`, - sql`make_date(2019 , extract(month from current_date)::integer, extract(day from current_date)::integer) between "hideEndingAt" and "hideStartingAt"` + eb('hideStartingAt', 'is', null), + sql`make_date(2020 , extract(month from current_date)::integer, extract(day from current_date)::integer) between "hideEndingAt" and "hideStartingAt"`, + sql`make_date(2019 , extract(month from current_date)::integer, extract(day from current_date)::integer) between "hideEndingAt" and "hideStartingAt"` ]) ) .execute() diff --git a/packages/server/fileStorage/GCSManager.ts b/packages/server/fileStorage/GCSManager.ts index 0a2a6a2dba9..a3a1cb4d03e 100644 --- a/packages/server/fileStorage/GCSManager.ts +++ b/packages/server/fileStorage/GCSManager.ts @@ -1,6 +1,7 @@ import {sign} from 'jsonwebtoken' import mime from 'mime-types' import path from 'path' +import {Logger} from '../utils/Logger' import FileStoreManager from './FileStoreManager' interface CloudKey { @@ -132,7 +133,7 @@ export default class GCSManager extends FileStoreManager { // https://github.com/nodejs/undici/issues/583#issuecomment-1577475664 // GCS will cause undici to error randomly with `SocketError: other side closed` `code: 'UND_ERR_SOCKET'` if ((e as any).cause?.code === 'UND_ERR_SOCKET') { - console.log(' Retrying GCS Post:', fullPath) + Logger.log(' Retrying GCS Post:', fullPath) await this.putFile(file, fullPath) } } diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index 53f7a0325c2..e009f7351c2 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -16,6 +16,7 @@ import removeSuggestedAction from '../../safeMutations/removeSuggestedAction' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' +import {Logger} from '../../utils/Logger' import publish from '../../utils/publish' import standardError from '../../utils/standardError' import {DataLoaderWorker, GQLContext} from '../graphql' @@ -230,7 +231,7 @@ export default { const updatedTaskIds = (result && result.updatedTaskIds) || [] analytics.checkInEnd(completedCheckIn, meetingMembers, team, dataLoader) - sendNewMeetingSummary(completedCheckIn, context).catch(console.log) + sendNewMeetingSummary(completedCheckIn, context).catch(Logger.log) checkTeamsLimit(team.orgId, dataLoader) const events = teamMembers.map( diff --git a/packages/server/graphql/mutations/endSprintPoker.ts b/packages/server/graphql/mutations/endSprintPoker.ts index 220a9d90aa7..21d18816b99 100644 --- a/packages/server/graphql/mutations/endSprintPoker.ts +++ b/packages/server/graphql/mutations/endSprintPoker.ts @@ -20,6 +20,7 @@ import sendNewMeetingSummary from './helpers/endMeeting/sendNewMeetingSummary' import {IntegrationNotifier} from './helpers/notifications/IntegrationNotifier' import removeEmptyTasks from './helpers/removeEmptyTasks' import updateTeamInsights from './helpers/updateTeamInsights' +import {Logger} from '../../utils/Logger' export default { type: new GraphQLNonNull(EndSprintPokerPayload), @@ -114,7 +115,7 @@ export default { analytics.sprintPokerEnd(completedMeeting, meetingMembers, template, dataLoader) const isKill = !!(phase && phase.phaseType !== 'ESTIMATE') if (!isKill) { - sendNewMeetingSummary(completedMeeting, context).catch(console.log) + sendNewMeetingSummary(completedMeeting, context).catch(Logger.log) checkTeamsLimit(team.orgId, dataLoader) } const events = teamMembers.map( diff --git a/packages/server/graphql/mutations/helpers/activatePrevSlackAuth.ts b/packages/server/graphql/mutations/helpers/activatePrevSlackAuth.ts index b2e39709458..030d4a30446 100644 --- a/packages/server/graphql/mutations/helpers/activatePrevSlackAuth.ts +++ b/packages/server/graphql/mutations/helpers/activatePrevSlackAuth.ts @@ -2,6 +2,7 @@ import ms from 'ms' import getRethink from '../../../database/rethinkDriver' import SlackServerManager from '../../../utils/SlackServerManager' import {upsertNotifications} from '../addSlackAuth' +import {Logger} from '../../../utils/Logger' const activatePrevSlackAuth = async (userId: string, teamId: string) => { const r = await getRethink() @@ -29,7 +30,7 @@ const activatePrevSlackAuth = async (userId: string, teamId: string) => { const manager = new SlackServerManager(botAccessToken) const authRes = await manager.isValidAuthToken(botAccessToken) if (!authRes.ok) { - console.error(authRes.error) + Logger.error(authRes.error) return } diff --git a/packages/server/graphql/mutations/helpers/generateGroups.ts b/packages/server/graphql/mutations/helpers/generateGroups.ts index 6d5e972e3b6..f487119ecc0 100644 --- a/packages/server/graphql/mutations/helpers/generateGroups.ts +++ b/packages/server/graphql/mutations/helpers/generateGroups.ts @@ -6,6 +6,7 @@ import {AutogroupReflectionGroupType} from '../../../database/types/MeetingRetro import {SubscriptionChannel} from '../../../../client/types/constEnums' import publish from '../../../utils/publish' import {analytics} from '../../../utils/analytics/analytics' +import {Logger} from '../../../utils/Logger' const generateGroups = async ( reflections: Reflection[], @@ -24,13 +25,13 @@ const generateGroups = async ( const themes = await manager.generateThemes(groupReflectionsInput) if (!themes) { - console.warn('ChatGPT was unable to generate themes') + Logger.warn('ChatGPT was unable to generate themes') return } const groupedReflections = await manager.groupReflections(groupReflectionsInput, themes) if (!groupedReflections) { - console.warn('ChatGPT was unable to group the reflections') + Logger.warn('ChatGPT was unable to group the reflections') return } const autogroupReflectionGroups: AutogroupReflectionGroupType[] = [] diff --git a/packages/server/graphql/mutations/helpers/getCCFromCustomer.ts b/packages/server/graphql/mutations/helpers/getCCFromCustomer.ts index fe6add91d77..de2bdf41696 100644 --- a/packages/server/graphql/mutations/helpers/getCCFromCustomer.ts +++ b/packages/server/graphql/mutations/helpers/getCCFromCustomer.ts @@ -1,6 +1,7 @@ import Stripe from 'stripe' import {getStripeManager} from '../../../utils/stripe' import {stripeCardToDBCard} from './stripeCardToDBCard' +import {Logger} from '../../../utils/Logger' export default async function getCCFromCustomer( customer: Stripe.Customer | Stripe.DeletedCustomer @@ -16,7 +17,7 @@ export default async function getCCFromCustomer( // customers that used Stripe Elements have default_payment_method: https://stripe.com/docs/payments/payment-methods/transitioning?locale=en-GB const cardRes = await manager.retrieveDefaultCardDetails(customer.id) if (cardRes instanceof Error) { - console.error(cardRes) + Logger.error(cardRes) return undefined } return stripeCardToDBCard(cardRes) diff --git a/packages/server/graphql/mutations/helpers/removeFromOrg.ts b/packages/server/graphql/mutations/helpers/removeFromOrg.ts index 3f0efabca2d..6d2747e227b 100644 --- a/packages/server/graphql/mutations/helpers/removeFromOrg.ts +++ b/packages/server/graphql/mutations/helpers/removeFromOrg.ts @@ -8,6 +8,7 @@ import {DataLoaderWorker} from '../../graphql' import removeTeamMember from './removeTeamMember' import resolveDowngradeToStarter from './resolveDowngradeToStarter' import {RDatum} from '../../../database/stricterR' +import {Logger} from '../../../utils/Logger' const removeFromOrg = async ( userId: string, @@ -93,7 +94,7 @@ const removeFromOrg = async ( try { await adjustUserCount(userId, orgId, InvoiceItemType.REMOVE_USER, dataLoader) } catch (e) { - console.log(e) + Logger.log(e) } await setUserTierForUserIds([userId]) return { diff --git a/packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts b/packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts index e2ebb39d476..ce82907ecfb 100644 --- a/packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts +++ b/packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts @@ -3,6 +3,7 @@ import Organization from '../../../database/types/Organization' import getKysely from '../../../postgres/getKysely' import updateTeamByOrgId from '../../../postgres/queries/updateTeamByOrgId' import {analytics} from '../../../utils/analytics/analytics' +import {Logger} from '../../../utils/Logger' import setTierForOrgUsers from '../../../utils/setTierForOrgUsers' import setUserTierForOrgId from '../../../utils/setUserTierForOrgId' import {getStripeManager} from '../../../utils/stripe' @@ -22,7 +23,7 @@ const resolveDowngradeToStarter = async ( try { await manager.deleteSubscription(stripeSubscriptionId) } catch (e) { - console.log(e) + Logger.log(e) } const [org] = await Promise.all([ diff --git a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts index 7890e47ef07..cd116c65ea8 100644 --- a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts @@ -29,6 +29,7 @@ import removeEmptyTasks from './removeEmptyTasks' import updateQualAIMeetingsCount from './updateQualAIMeetingsCount' import gatherInsights from './gatherInsights' import NotificationMentioned from '../../../database/types/NotificationMentioned' +import {Logger} from '../../../utils/Logger' const getTranscription = async (recallBotId?: string | null) => { if (!recallBotId) return @@ -245,7 +246,7 @@ const summarizeRetroMeeting = async (meeting: MeetingRetrospective, context: Int dataLoader.get('newMeetings').clear(meetingId) // wait for whole meeting summary to be generated before sending summary email and updating qualAIMeetingCount - sendNewMeetingSummary(meeting, context).catch(console.log) + sendNewMeetingSummary(meeting, context).catch(Logger.log) updateQualAIMeetingsCount(meetingId, teamId, dataLoader) // wait for meeting stats to be generated before sending Slack notification IntegrationNotifier.endMeeting(dataLoader, meetingId, teamId) diff --git a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts index 5b58f178a87..e8e6f1b6cb5 100644 --- a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts +++ b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts @@ -14,6 +14,7 @@ import updateTeamInsights from './updateTeamInsights' import generateStandupMeetingSummary from './generateStandupMeetingSummary' import updateQualAIMeetingsCount from './updateQualAIMeetingsCount' import gatherInsights from './gatherInsights' +import {Logger} from '../../../utils/Logger' const summarizeTeamPrompt = async (meeting: MeetingTeamPrompt, context: InternalContext) => { const {dataLoader} = context @@ -31,7 +32,7 @@ const summarizeTeamPrompt = async (meeting: MeetingTeamPrompt, context: Internal dataLoader.get('newMeetings').clear(meeting.id) // wait for whole meeting summary to be generated before sending summary email and updating qualAIMeetingCount - sendNewMeetingSummary(meeting, context).catch(console.log) + sendNewMeetingSummary(meeting, context).catch(Logger.log) updateQualAIMeetingsCount(meeting.id, meeting.teamId, dataLoader) // wait for meeting stats to be generated before sending Slack notification IntegrationNotifier.endMeeting(dataLoader, meeting.id, meeting.teamId) diff --git a/packages/server/graphql/mutations/moveTeamToOrg.ts b/packages/server/graphql/mutations/moveTeamToOrg.ts index 06be3517417..7fa25b2b41d 100644 --- a/packages/server/graphql/mutations/moveTeamToOrg.ts +++ b/packages/server/graphql/mutations/moveTeamToOrg.ts @@ -13,6 +13,7 @@ import standardError from '../../utils/standardError' import {DataLoaderWorker, GQLContext} from '../graphql' import isValid from '../isValid' import getKysely from '../../postgres/getKysely' +import {Logger} from '../../utils/Logger' const MAX_NUM_TEAMS = 40 @@ -168,7 +169,7 @@ export default { const successes = results.filter((result) => typeof result === 'string') const failures = results.filter((result) => typeof result !== 'string') const successStr = successes.join('\n') - console.error('failures', failures) + Logger.error('failures', failures) return successStr } } diff --git a/packages/server/graphql/mutations/navigateMeeting.ts b/packages/server/graphql/mutations/navigateMeeting.ts index 2f833970921..70340fa2401 100644 --- a/packages/server/graphql/mutations/navigateMeeting.ts +++ b/packages/server/graphql/mutations/navigateMeeting.ts @@ -11,6 +11,7 @@ import {GQLContext} from '../graphql' import NavigateMeetingPayload from '../types/NavigateMeetingPayload' import handleCompletedStage from './helpers/handleCompletedStage' import removeScheduledJobs from './helpers/removeScheduledJobs' +import {Logger} from '../../utils/Logger' export default { type: new GraphQLNonNull(NavigateMeetingPayload), @@ -84,7 +85,7 @@ export default { phaseCompleteData = await handleCompletedStage(stage, meeting, dataLoader) if (stage.scheduledEndTime) { // not critical, no await needed - removeScheduledJobs(stage.scheduledEndTime, {meetingId}).catch(console.error) + removeScheduledJobs(stage.scheduledEndTime, {meetingId}).catch(Logger.error) stage.scheduledEndTime = null } } diff --git a/packages/server/graphql/mutations/selectTemplate.ts b/packages/server/graphql/mutations/selectTemplate.ts index 5877f848b01..4d0ee10f092 100644 --- a/packages/server/graphql/mutations/selectTemplate.ts +++ b/packages/server/graphql/mutations/selectTemplate.ts @@ -8,6 +8,7 @@ import standardError from '../../utils/standardError' import {GQLContext} from '../graphql' import SelectTemplatePayload from '../types/SelectTemplatePayload' import {getFeatureTier} from '../types/helpers/getFeatureTier' +import {Logger} from '../../utils/Logger' const selectTemplate = { description: 'Set the selected template for the upcoming retro meeting', @@ -37,7 +38,7 @@ const selectTemplate = { ]) if (!template || !template.isActive) { - console.log('no template', selectedTemplateId, template) + Logger.log('no template', selectedTemplateId, template) return standardError(new Error('Template not found'), {userId: viewerId}) } diff --git a/packages/server/graphql/mutations/updateAzureDevOpsDimensionField.ts b/packages/server/graphql/mutations/updateAzureDevOpsDimensionField.ts index a366c1afc9e..7e27ad66e14 100644 --- a/packages/server/graphql/mutations/updateAzureDevOpsDimensionField.ts +++ b/packages/server/graphql/mutations/updateAzureDevOpsDimensionField.ts @@ -5,6 +5,7 @@ import upsertAzureDevOpsDimensionFieldMap, { AzureDevOpsFieldMapProps } from '../../postgres/queries/upsertAzureDevOpsDimensionFieldMap' import {isTeamMember} from '../../utils/authorization' +import {Logger} from '../../utils/Logger' import publish from '../../utils/publish' import {GQLContext} from '../graphql' import UpdateAzureDevOpsDimensionFieldPayload from '../types/UpdateAzureDevOpsDimensionFieldPayload' @@ -57,7 +58,7 @@ const updateAzureDevOpsDimensionField = { }, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { - //console.log(`Inside updateAzureDevOpsDimensionField`) + //Logger.log(`Inside updateAzureDevOpsDimensionField`) const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -91,7 +92,7 @@ const updateAzureDevOpsDimensionField = { } as AzureDevOpsFieldMapProps await upsertAzureDevOpsDimensionFieldMap(props) } catch (e) { - console.log(e) + Logger.log(e) } const data = {teamId, meetingId} diff --git a/packages/server/graphql/mutations/updateGitHubDimensionField.ts b/packages/server/graphql/mutations/updateGitHubDimensionField.ts index d6077e4d01d..703f9e601e9 100644 --- a/packages/server/graphql/mutations/updateGitHubDimensionField.ts +++ b/packages/server/graphql/mutations/updateGitHubDimensionField.ts @@ -3,6 +3,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import MeetingPoker from '../../database/types/MeetingPoker' import upsertGitHubDimensionFieldMap from '../../postgres/queries/upsertGitHubDimensionFieldMap' import {isTeamMember} from '../../utils/authorization' +import {Logger} from '../../utils/Logger' import publish from '../../utils/publish' import {GQLContext} from '../graphql' import UpdateGitHubDimensionFieldPayload from '../types/UpdateGitHubDimensionFieldPayload' @@ -66,7 +67,7 @@ const updateGitHubDimensionField = { try { await upsertGitHubDimensionFieldMap(teamId, dimensionName, nameWithOwner, labelTemplate) } catch (e) { - console.log(e) + Logger.log(e) } const data = {meetingId, teamId} diff --git a/packages/server/graphql/private/mutations/autopauseUsers.ts b/packages/server/graphql/private/mutations/autopauseUsers.ts index ed92c2e95c9..ba8e3439b0b 100644 --- a/packages/server/graphql/private/mutations/autopauseUsers.ts +++ b/packages/server/graphql/private/mutations/autopauseUsers.ts @@ -3,6 +3,7 @@ import adjustUserCount from '../../../billing/helpers/adjustUserCount' import getRethink from '../../../database/rethinkDriver' import getUserIdsToPause from '../../../postgres/queries/getUserIdsToPause' import {MutationResolvers} from '../resolverTypes' +import {Logger} from '../../../utils/Logger' const autopauseUsers: MutationResolvers['autopauseUsers'] = async ( _source, @@ -32,7 +33,7 @@ const autopauseUsers: MutationResolvers['autopauseUsers'] = async ( try { return await adjustUserCount(userId, orgIds, InvoiceItemType.AUTO_PAUSE_USER, dataLoader) } catch (e) { - console.warn(`Error adjusting user count`) + Logger.warn(`Error adjusting user count`) } return undefined }) diff --git a/packages/server/graphql/private/mutations/connectSocket.ts b/packages/server/graphql/private/mutations/connectSocket.ts index c105d0ef180..4461a506f27 100644 --- a/packages/server/graphql/private/mutations/connectSocket.ts +++ b/packages/server/graphql/private/mutations/connectSocket.ts @@ -6,6 +6,7 @@ import {analytics} from '../../../utils/analytics/analytics' import {getUserId} from '../../../utils/authorization' import getListeningUserIds, {RedisCommand} from '../../../utils/getListeningUserIds' import getRedis from '../../../utils/getRedis' +import {Logger} from '../../../utils/Logger' import publish from '../../../utils/publish' import {MutationResolvers} from '../resolverTypes' @@ -44,7 +45,7 @@ const connectSocket: MutationResolvers['connectSocket'] = async ( .getAll(userId, {index: 'userId'}) .filter({removedAt: null, inactive: true})('orgId') .run() - adjustUserCount(userId, orgIds, InvoiceItemType.UNPAUSE_USER, dataLoader).catch(console.log) + adjustUserCount(userId, orgIds, InvoiceItemType.UNPAUSE_USER, dataLoader).catch(Logger.log) // TODO: re-identify } const datesAreOnSameDay = now.toDateString() === lastSeenAt.toDateString() diff --git a/packages/server/graphql/private/mutations/runScheduledJobs.ts b/packages/server/graphql/private/mutations/runScheduledJobs.ts index 923d4617119..faee38517ec 100644 --- a/packages/server/graphql/private/mutations/runScheduledJobs.ts +++ b/packages/server/graphql/private/mutations/runScheduledJobs.ts @@ -8,6 +8,7 @@ import publish from '../../../utils/publish' import {DataLoaderWorker} from '../../graphql' import {IntegrationNotifier} from '../../mutations/helpers/notifications/IntegrationNotifier' import {MutationResolvers} from '../resolverTypes' +import {Logger} from '../../../utils/Logger' const processMeetingStageTimeLimits = async ( job: ScheduledJobMeetingStageTimeLimit, @@ -48,9 +49,9 @@ const processJob = async (job: ScheduledJobUnion, dataLoader: DataLoaderWorker) return processMeetingStageTimeLimits( job as ScheduledJobMeetingStageTimeLimit, dataLoader - ).catch(console.error) + ).catch(Logger.error) } else if (job.type === 'LOCK_ORGANIZATION' || job.type === 'WARN_ORGANIZATION') { - return processTeamsLimitsJob(job as ScheduledTeamLimitsJob, dataLoader).catch(console.error) + return processTeamsLimitsJob(job as ScheduledTeamLimitsJob, dataLoader).catch(Logger.error) } } @@ -73,7 +74,7 @@ const runScheduledJobs: MutationResolvers['runScheduledJobs'] = async ( const {runAt} = job const timeout = Math.max(0, runAt.getTime() - now.getTime()) setTimeout(() => { - processJob(job, dataLoader).catch(console.error) + processJob(job, dataLoader).catch(Logger.error) }, timeout) }) diff --git a/packages/server/graphql/public/mutations/acceptRequestToJoinDomain.ts b/packages/server/graphql/public/mutations/acceptRequestToJoinDomain.ts index b7920912068..ea12206cdd5 100644 --- a/packages/server/graphql/public/mutations/acceptRequestToJoinDomain.ts +++ b/packages/server/graphql/public/mutations/acceptRequestToJoinDomain.ts @@ -14,6 +14,7 @@ import publish from '../../../utils/publish' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import DomainJoinRequestId from 'parabol-client/shared/gqlIds/DomainJoinRequestId' import {getUserById} from '../../../postgres/queries/getUsersByIds' +import {Logger} from '../../../utils/Logger' // TODO (EXPERIMENT: prompt-to-join-org): some parts are borrowed from acceptTeamInvitation, create generic functions const acceptRequestToJoinDomain: MutationResolvers['acceptRequestToJoinDomain'] = async ( @@ -106,7 +107,7 @@ const acceptRequestToJoinDomain: MutationResolvers['acceptRequestToJoinDomain'] try { await adjustUserCount(userId, orgId, InvoiceItemType.ADD_USER, dataLoader) } catch (e) { - console.log(e) + Logger.log(e) } await setUserTierForUserIds([userId]) } diff --git a/packages/server/graphql/public/mutations/helpers/SAMLHelpers/getURLWithSAMLRequestParam.ts b/packages/server/graphql/public/mutations/helpers/SAMLHelpers/getURLWithSAMLRequestParam.ts index bf80ce33ac4..d3fa3b7e98f 100644 --- a/packages/server/graphql/public/mutations/helpers/SAMLHelpers/getURLWithSAMLRequestParam.ts +++ b/packages/server/graphql/public/mutations/helpers/SAMLHelpers/getURLWithSAMLRequestParam.ts @@ -2,13 +2,14 @@ import {v4 as uuid} from 'uuid' import zlib from 'zlib' const getURLWithSAMLRequestParam = (destination: string, slug: string) => { + const issuer = process.env.SAML_ISSUER || `https://${process.env.HOST}/saml-metadata/${slug}` const template = ` - https://${process.env.HOST}/saml-metadata/${slug} + ${issuer} ` diff --git a/packages/server/graphql/public/mutations/updateGitLabDimensionField.ts b/packages/server/graphql/public/mutations/updateGitLabDimensionField.ts index f03ebde9493..88ca97eb167 100644 --- a/packages/server/graphql/public/mutations/updateGitLabDimensionField.ts +++ b/packages/server/graphql/public/mutations/updateGitLabDimensionField.ts @@ -2,6 +2,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import MeetingPoker from '../../../database/types/MeetingPoker' import upsertGitLabDimensionFieldMap from '../../../postgres/queries/upsertGitLabDimensionFieldMap' import {isTeamMember} from '../../../utils/authorization' +import {Logger} from '../../../utils/Logger' import publish from '../../../utils/publish' import {MutationResolvers} from '../resolverTypes' import {getUserId} from './../../../utils/authorization' @@ -40,7 +41,7 @@ const updateGitLabDimensionField: MutationResolvers['updateGitLabDimensionField' const {providerId} = gitlabAuth await upsertGitLabDimensionFieldMap(teamId, dimensionName, projectId, providerId, labelTemplate) } catch (e) { - console.log(e) + Logger.log(e) } const data = {meetingId, teamId} diff --git a/packages/server/graphql/public/types/TeamHealthStage.ts b/packages/server/graphql/public/types/TeamHealthStage.ts index 29c7834c877..f98202ae131 100644 --- a/packages/server/graphql/public/types/TeamHealthStage.ts +++ b/packages/server/graphql/public/types/TeamHealthStage.ts @@ -2,6 +2,7 @@ import {TeamHealthStageResolvers} from '../resolverTypes' import {getUserId} from '../../../utils/authorization' import TeamHealthStageDB from '../../../database/types/TeamHealthStage' import isValid from '../../isValid' +import {Logger} from '../../../utils/Logger' export type TeamHealthStageSource = TeamHealthStageDB & { meetingId: string @@ -21,7 +22,7 @@ const TeamHealthStage: TeamHealthStageResolvers = { }, readyCount: async ({meetingId, readyToAdvance}, _args, {dataLoader}, ref) => { if (!readyToAdvance) return 0 - if (!meetingId) console.log('no meetingid', ref) + if (!meetingId) Logger.log('no meetingid', ref) const meeting = await dataLoader.get('newMeetings').load(meetingId) const {facilitatorUserId} = meeting return readyToAdvance.filter((userId) => userId !== facilitatorUserId).length diff --git a/packages/server/graphql/queries/helpers/fetchGitHubRepos.ts b/packages/server/graphql/queries/helpers/fetchGitHubRepos.ts index 17fd935ae64..14b951248c0 100644 --- a/packages/server/graphql/queries/helpers/fetchGitHubRepos.ts +++ b/packages/server/graphql/queries/helpers/fetchGitHubRepos.ts @@ -4,6 +4,7 @@ import getGitHubRequest from '../../../utils/getGitHubRequest' import getRepositories from '../../../utils/githubQueries/getRepositories.graphql' import {DataLoaderWorker} from '../../graphql' import {GQLContext} from './../../graphql' +import {Logger} from '../../../utils/Logger' export interface GitHubRepo { id: string @@ -25,7 +26,7 @@ const fetchGitHubRepos = async ( const githubRequest = getGitHubRequest(info, context, {accessToken}) const [data, error] = await githubRequest(getRepositories) if (error) { - console.error(error.message) + Logger.error(error.message) return [] } const {viewer} = data diff --git a/packages/server/graphql/queries/helpers/fetchGitLabProjects.ts b/packages/server/graphql/queries/helpers/fetchGitLabProjects.ts index 8ff83e7a8e3..72da38eddb3 100644 --- a/packages/server/graphql/queries/helpers/fetchGitLabProjects.ts +++ b/packages/server/graphql/queries/helpers/fetchGitLabProjects.ts @@ -2,6 +2,7 @@ import {GraphQLResolveInfo} from 'graphql' import {isNotNull} from 'parabol-client/utils/predicates' import GitLabServerManager from '../../../integrations/gitlab/GitLabServerManager' import {GQLContext} from '../../graphql' +import {Logger} from '../../../utils/Logger' const fetchGitLabProjects = async ( teamId: string, @@ -18,7 +19,7 @@ const fetchGitLabProjects = async ( const manager = new GitLabServerManager(auth, context, info, provider.serverBaseUrl) const [data, error] = await manager.getProjects({}) if (error) { - console.error(error.message) + Logger.error(error.message) return [] } return ( diff --git a/packages/server/graphql/types/NewMeetingStage.ts b/packages/server/graphql/types/NewMeetingStage.ts index 5eecb36a46f..0247e4ebbf2 100644 --- a/packages/server/graphql/types/NewMeetingStage.ts +++ b/packages/server/graphql/types/NewMeetingStage.ts @@ -11,6 +11,7 @@ import GenericMeetingPhase, { NewMeetingPhaseTypeEnum as NewMeetingPhaseTypeEnumType } from '../../database/types/GenericMeetingPhase' import {getUserId} from '../../utils/authorization' +import {Logger} from '../../utils/Logger' import {GQLContext} from '../graphql' import GraphQLISO8601Type from './GraphQLISO8601Type' import NewMeeting from './NewMeeting' @@ -112,7 +113,7 @@ export const newMeetingStageFields = () => ({ ref: any ) => { if (!readyToAdvance) return 0 - if (!meetingId) console.log('no meetingid', ref) + if (!meetingId) Logger.log('no meetingid', ref) const meeting = await dataLoader.get('newMeetings').load(meetingId) const {facilitatorUserId} = meeting return readyToAdvance.filter((userId: string) => userId !== facilitatorUserId).length diff --git a/packages/server/graphql/types/PokerMeeting.ts b/packages/server/graphql/types/PokerMeeting.ts index f9262f8a919..fd0116d26f2 100644 --- a/packages/server/graphql/types/PokerMeeting.ts +++ b/packages/server/graphql/types/PokerMeeting.ts @@ -1,6 +1,7 @@ import {GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType} from 'graphql' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import {getUserId} from '../../utils/authorization' +import {Logger} from '../../utils/Logger' import {GQLContext} from '../graphql' import NewMeeting from './NewMeeting' import PokerMeetingMember from './PokerMeetingMember' @@ -39,7 +40,7 @@ const PokerMeeting = new GraphQLObjectType({ resolve: async ({id: meetingId}, {storyId: taskId}, {dataLoader}) => { const task = await dataLoader.get('tasks').load(taskId) if (task.meetingId !== meetingId) { - console.log('naughty storyId supplied to PokerMeeting') + Logger.log('naughty storyId supplied to PokerMeeting') return null } return task diff --git a/packages/server/package.json b/packages/server/package.json index c1bde9cbe3f..625dc427b3f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.17.0", + "version": "7.19.3", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -124,7 +124,7 @@ "oauth-1.0a": "^2.2.6", "openai": "^4.24.1", "oy-vey": "^0.11.0", - "parabol-client": "7.17.0", + "parabol-client": "7.19.3", "pg": "^8.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/packages/server/postgres/getKysely.ts b/packages/server/postgres/getKysely.ts index 0a8632a3eb6..f2824a800d0 100644 --- a/packages/server/postgres/getKysely.ts +++ b/packages/server/postgres/getKysely.ts @@ -10,6 +10,13 @@ const getKysely = () => { dialect: new PostgresDialect({ pool: pg }) + // query logging, if you'd like it: + // log(event) { + // if (event.level === 'query') { + // console.log(event.query.sql) + // console.log(event.query.parameters) + // } + // } }) } return kysely diff --git a/packages/server/postgres/migrations/1708127504000_updateEmbeddingMetadata.ts b/packages/server/postgres/migrations/1708127504000_updateEmbeddingMetadata.ts new file mode 100644 index 00000000000..3c93617c0b1 --- /dev/null +++ b/packages/server/postgres/migrations/1708127504000_updateEmbeddingMetadata.ts @@ -0,0 +1,20 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + ALTER TABLE "EmbeddingsMetadata" RENAME COLUMN "embedText" TO "fullText"; + `) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + ALTER TABLE "EmbeddingsMetadata" RENAME COLUMN "fullText" TO "embedText"; + `) + await client.end() +} diff --git a/packages/server/postgres/queries/upsertAzureDevOpsDimensionFieldMap.ts b/packages/server/postgres/queries/upsertAzureDevOpsDimensionFieldMap.ts index e19e9ad7c83..d5099ed387d 100644 --- a/packages/server/postgres/queries/upsertAzureDevOpsDimensionFieldMap.ts +++ b/packages/server/postgres/queries/upsertAzureDevOpsDimensionFieldMap.ts @@ -1,3 +1,4 @@ +import {Logger} from '../../utils/Logger' import getPg from '../getPg' import {upsertAzureDevOpsDimensionFieldMapQuery} from './generated/upsertAzureDevOpsDimensionFieldMapQuery' @@ -22,7 +23,7 @@ const upsertAzureDevOpsDimensionFieldMap = async (props: AzureDevOpsFieldMapProp projectKey, workItemType } = props - console.log(`Inside upsertAzureDevOpsDimensionFieldMap - props:${JSON.stringify(props)}`) + Logger.log(`Inside upsertAzureDevOpsDimensionFieldMap - props:${JSON.stringify(props)}`) return upsertAzureDevOpsDimensionFieldMapQuery.run( { fieldMap: { diff --git a/packages/server/safeMutations/acceptTeamInvitation.ts b/packages/server/safeMutations/acceptTeamInvitation.ts index 98563d9270f..be5ec868c6d 100644 --- a/packages/server/safeMutations/acceptTeamInvitation.ts +++ b/packages/server/safeMutations/acceptTeamInvitation.ts @@ -6,6 +6,7 @@ import generateUID from '../generateUID' import {DataLoaderWorker} from '../graphql/graphql' import {Team} from '../postgres/queries/getTeamsByIds' import getNewTeamLeadUserId from '../safeQueries/getNewTeamLeadUserId' +import {Logger} from '../utils/Logger' import setUserTierForUserIds from '../utils/setUserTierForUserIds' import addTeamIdToTMS from './addTeamIdToTMS' import insertNewTeamMember from './insertNewTeamMember' @@ -92,7 +93,7 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data try { await adjustUserCount(userId, orgId, InvoiceItemType.ADD_USER, dataLoader) } catch (e) { - console.log(e) + Logger.log(e) } await setUserTierForUserIds([userId]) } diff --git a/packages/server/safetyPatchRes.ts b/packages/server/safetyPatchRes.ts index d48e736b5cf..aa974ebb88f 100644 --- a/packages/server/safetyPatchRes.ts +++ b/packages/server/safetyPatchRes.ts @@ -1,4 +1,5 @@ import {HttpResponse, RecognizedString} from 'uWebSockets.js' +import {Logger} from './utils/Logger' type Header = [key: RecognizedString, value: RecognizedString] @@ -36,7 +37,7 @@ const safetyPatchRes = (res: HttpResponse) => { res._end = res.end res.end = (body?: RecognizedString) => { if (res.done) { - console.warn(`uWS: Called end after done`) + Logger.warn(`uWS: Called end after done`) } if (res.done || res.aborted) return res res.done = true @@ -46,7 +47,7 @@ const safetyPatchRes = (res: HttpResponse) => { res._close = res.close res.close = () => { if (res.done) { - console.warn(`uWS: Called close after done`) + Logger.warn(`uWS: Called close after done`) } if (res.done || res.aborted) return res res.done = true @@ -61,7 +62,7 @@ const safetyPatchRes = (res: HttpResponse) => { res._tryEnd = res.tryEnd res.tryEnd = (fullBodyOrChunk: RecognizedString, totalSize: number) => { if (res.done) { - console.warn(`uWS: Called tryEnd after done`) + Logger.warn(`uWS: Called tryEnd after done`) } if (res.done || res.aborted) return [true, true] return flush(() => res._tryEnd(fullBodyOrChunk, totalSize)) @@ -70,7 +71,7 @@ const safetyPatchRes = (res: HttpResponse) => { res._write = res.write res.write = (chunk: RecognizedString) => { if (res.done) { - console.warn(`uWS: Called write after done`) + Logger.warn(`uWS: Called write after done`) } if (res.done || res.aborted) return res return res._write(chunk) @@ -79,7 +80,7 @@ const safetyPatchRes = (res: HttpResponse) => { res._writeHeader = res.writeHeader res.writeHeader = (key: RecognizedString, value: RecognizedString) => { if (res.done) { - console.warn(`uWS: Called writeHeader after done`) + Logger.warn(`uWS: Called writeHeader after done`) } res.headers.push([key, value]) return res @@ -88,7 +89,7 @@ const safetyPatchRes = (res: HttpResponse) => { res._writeStatus = res.writeStatus res.writeStatus = (status: RecognizedString) => { if (res.done) { - console.error(`uWS: Called writeStatus after done ${status}`) + Logger.error(`uWS: Called writeStatus after done ${status}`) } res.status = status return res @@ -97,7 +98,7 @@ const safetyPatchRes = (res: HttpResponse) => { res._upgrade = res.upgrade res.upgrade = (...args) => { if (res.done) { - console.error(`uWS: Called upgrade after done`) + Logger.error(`uWS: Called upgrade after done`) } if (res.done || res.aborted) return return res._cork(() => { @@ -108,7 +109,7 @@ const safetyPatchRes = (res: HttpResponse) => { res._getRemoteAddressAsText = res.getRemoteAddressAsText res.getRemoteAddressAsText = () => { if (res.done) { - console.error(`uWS: Called getRemoteAddressAsText after done`) + Logger.error(`uWS: Called getRemoteAddressAsText after done`) } if (res.done || res.aborted) return Buffer.from('') return res._getRemoteAddressAsText() diff --git a/packages/server/utils/AtlassianServerManager.ts b/packages/server/utils/AtlassianServerManager.ts index dcc29a4a3b9..d84a448f7be 100644 --- a/packages/server/utils/AtlassianServerManager.ts +++ b/packages/server/utils/AtlassianServerManager.ts @@ -11,25 +11,7 @@ import { OAuth2AuthorizationParams, OAuth2RefreshAuthorizationParams } from '../integrations/OAuth2Manager' - -import tracer from 'dd-trace' -import formats from 'dd-trace/ext/formats' -import util from 'util' - -type LogLevel = 'error' | 'warn' | 'info' | 'debug' -function trace(level: LogLevel, message: any, ...optionalParameters: any[]) { - const span = tracer.scope().active() - const time = new Date().toISOString() - const record = {time, level, message: util.format(message, optionalParameters)} - - if (span) { - tracer.inject(span.context(), formats.LOG, record) - } - - console.log(JSON.stringify(record)) -} - -const log = trace.bind(null, 'info') +import {Logger} from './Logger' export interface JiraUser { self: string @@ -343,7 +325,7 @@ class AtlassianServerManager extends AtlassianManager { } else { callback(null, {cloudId, newProjects: res.values}) if (res.nextPage) { - await this.getPaginatedProjects(cloudId, res.nextPage, callback).catch(console.error) + await this.getPaginatedProjects(cloudId, res.nextPage, callback).catch(Logger.error) } } } @@ -355,7 +337,7 @@ class AtlassianServerManager extends AtlassianManager { cloudId, `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/project/search?orderBy=name`, callback - ).catch(console.error) + ).catch(Logger.error) }) ) } @@ -387,7 +369,7 @@ class AtlassianServerManager extends AtlassianManager { })) projects.push(...pagedProjects) if (res.nextPage) { - log('AtlassianServerManager.getAllProjects fetching more results', res.total) + Logger.log('AtlassianServerManager.getAllProjects fetching more results', res.total) return getProjectPage(cloudId, res.nextPage) } } @@ -402,7 +384,7 @@ class AtlassianServerManager extends AtlassianManager { ) if (error) { - log('getAllProjects ERROR:', error) + Logger.log('getAllProjects ERROR:', error) } return projects } diff --git a/packages/server/utils/Logger.ts b/packages/server/utils/Logger.ts new file mode 100644 index 00000000000..3cca7489c69 --- /dev/null +++ b/packages/server/utils/Logger.ts @@ -0,0 +1,35 @@ +import tracer from 'dd-trace' +import formats from 'dd-trace/ext/formats' +import util from 'util' + +type LogLevel = 'error' | 'warn' | 'info' | 'debug' +const LogFun = { + error: console.error, + warn: console.warn, + info: console.info, + debug: console.debug +} satisfies Record + +function trace(level: LogLevel, message: any, ...optionalParameters: any[]) { + if (process.env.DD_LOGS_INJECTION !== 'true') { + return LogFun[level](message, ...optionalParameters) + } + + const span = tracer.scope().active() + const time = new Date().toISOString() + const record = {time, level, message: util.format(message, optionalParameters)} + + if (span) { + tracer.inject(span.context(), formats.LOG, record) + } + + LogFun[level](JSON.stringify(record)) +} + +export const Logger = { + log: trace.bind(null, 'info'), + error: trace.bind(null, 'error'), + warn: trace.bind(null, 'warn'), + info: trace.bind(null, 'info'), + debug: trace.bind(null, 'debug') +} diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 3cc48bb3868..251528770e2 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -3,6 +3,7 @@ import JSON5 from 'json5' import sendToSentry from './sendToSentry' import Reflection from '../database/types/Reflection' import {ModifyType} from '../graphql/public/resolverTypes' +import {Logger} from './Logger' type Prompt = { question: string @@ -188,7 +189,7 @@ class OpenAIServerManager { return themes.split(', ') } catch (e) { const error = e instanceof Error ? e : new Error('OpenAI failed to generate themes') - console.error(error.message) + Logger.error(error.message) sendToSentry(error) return null } @@ -226,7 +227,7 @@ class OpenAIServerManager { } catch (e) { const error = e instanceof Error ? e : new Error('OpenAI failed to generate the suggested template') - console.error(error.message) + Logger.error(error.message) sendToSentry(error) return null } diff --git a/packages/server/utils/RecallAIServerManager.ts b/packages/server/utils/RecallAIServerManager.ts index 5c30cfc1ceb..c5dc8e7f781 100644 --- a/packages/server/utils/RecallAIServerManager.ts +++ b/packages/server/utils/RecallAIServerManager.ts @@ -4,6 +4,7 @@ import {ExternalLinks} from '../../client/types/constEnums' import appOrigin from '../appOrigin' import {TranscriptBlock} from '../database/types/MeetingRetrospective' import sendToSentry from './sendToSentry' +import {Logger} from './Logger' const sdk = api('@recallai/v1.6#536jnqlf7d6blh') @@ -19,7 +20,7 @@ const getBase64Image = async () => { const base64Image = buffer.toString('base64') return base64Image } catch (error) { - console.error(error) + Logger.error(error) return null } } diff --git a/packages/server/utils/StaticServer.ts b/packages/server/utils/StaticServer.ts index f8cba007f00..2ba0fd3b3b6 100644 --- a/packages/server/utils/StaticServer.ts +++ b/packages/server/utils/StaticServer.ts @@ -3,6 +3,7 @@ import mime from 'mime-types' import path from 'path' import {brotliCompressSync} from 'zlib' import isCompressible from './isCompressible' +import {Logger} from './Logger' class StaticFileMeta { mtime: string size: number @@ -63,7 +64,7 @@ export default class StaticServer { } makePathnames(dirname, this.pathnames, '') } catch (e) { - console.log(e) + Logger.log(e) } }) } diff --git a/packages/server/utils/getSAMLURLFromEmail.ts b/packages/server/utils/getSAMLURLFromEmail.ts index a2e3fc65e11..2c07d74a06d 100644 --- a/packages/server/utils/getSAMLURLFromEmail.ts +++ b/packages/server/utils/getSAMLURLFromEmail.ts @@ -2,6 +2,13 @@ import base64url from 'base64url' import getSSODomainFromEmail from 'parabol-client/utils/getSSODomainFromEmail' import {URL} from 'url' import {DataLoaderWorker} from '../graphql/graphql' +import getKysely from '../postgres/getKysely' + +const isSingleTenantSSO = + process.env.AUTH_INTERNAL_DISABLED === 'true' && + process.env.AUTH_GOOGLE_DISABLED === 'true' && + process.env.AUTH_MICROSOFT_DISABLED === 'true' && + process.env.AUTH_SSO_DISABLED === 'false' const urlWithRelayState = (url: string, isInvited?: boolean | null) => { if (!isInvited) return url @@ -18,6 +25,19 @@ const getSAMLURLFromEmail = async ( ) => { const domainName = getSSODomainFromEmail(email) if (!domainName) return null + if (isSingleTenantSSO) { + // For PPMI use + const pg = getKysely() + const instanceURLres = await pg + .selectFrom('SAML') + .select('url') + .where('url', 'is not', null) + .limit(1) + .executeTakeFirst() + const instanceURL = instanceURLres?.url + if (!instanceURL) return null + return urlWithRelayState(instanceURL, isInvited) + } const saml = await dataLoader.get('samlByDomain').load(domainName) if (!saml) return null const {url} = saml diff --git a/packages/server/utils/publish.ts b/packages/server/utils/publish.ts index 5203852257f..b9c7defb2ff 100644 --- a/packages/server/utils/publish.ts +++ b/packages/server/utils/publish.ts @@ -1,4 +1,5 @@ import getPubSub from './getPubSub' +import {Logger} from './Logger' export interface SubOptions { mutatorId?: string // passing the socket id of the mutator will omit sending a message to that user @@ -18,7 +19,7 @@ const publish = ( const rootValue = {[subName]: {fieldName: type, [type]: payload}} getPubSub() .publish(`${topic}.${channel}`, {rootValue, executorServerId: SERVER_ID!, ...subOptions}) - .catch(console.error) + .catch(Logger.error) } export default publish diff --git a/packages/server/utils/stripe/StripeManager.ts b/packages/server/utils/stripe/StripeManager.ts index 0ab72ac35c3..28db42bd437 100644 --- a/packages/server/utils/stripe/StripeManager.ts +++ b/packages/server/utils/stripe/StripeManager.ts @@ -1,5 +1,6 @@ import {InvoiceItemType} from 'parabol-client/types/constEnums' import Stripe from 'stripe' +import {Logger} from '../Logger' import sendToSentry from '../sendToSentry' export default class StripeManager { @@ -15,7 +16,7 @@ export default class StripeManager { try { return this.stripe.webhooks.constructEvent(rawBody, signature, StripeManager.WEBHOOK_SECRET) } catch (e) { - console.log('StripeWebhookError:', e) + Logger.log('StripeWebhookError:', e) return null } } diff --git a/pm2.config.js b/pm2.config.js index 4d270b745ea..fdf09ea9ca2 100644 --- a/pm2.config.js +++ b/pm2.config.js @@ -30,6 +30,21 @@ module.exports = { NODE_ENV: 'production' } }, + { + name: 'Embedder', + script: 'dist/embedder.js', + instances: 1, + increment_var: 'SERVER_ID', + autorestart: true, + watch: false, + max_memory_restart: '4096M', + env: { + SERVER_ID: 5 + }, + env_production: { + NODE_ENV: 'production' + } + }, { name: 'GQL Executor', script: 'dist/gqlExecutor.js', @@ -39,7 +54,7 @@ module.exports = { watch: false, max_memory_restart: '24576M', env: { - SERVER_ID: 5 + SERVER_ID: 6 }, env_production: { NODE_ENV: 'production' diff --git a/pm2.dev.config.js b/pm2.dev.config.js index c5f04196dff..6cd457059ef 100644 --- a/pm2.dev.config.js +++ b/pm2.dev.config.js @@ -4,6 +4,19 @@ module.exports = { name: 'Webpack Servers', script: 'scripts/buildServers.js' }, + { + name: 'Socket Server', + script: 'scripts/runSocketServer.js', + // increase this to test scaling + instances: 1, + increment_var: 'SERVER_ID', + env: { + SERVER_ID: 0 + }, + watch: ['dev/web.js'], + // if the watched file doeesn't exist, wait for it instead of restarting + autorestart: false + }, { name: 'GraphQL Executor', script: 'scripts/runExecutor.js', @@ -15,24 +28,20 @@ module.exports = { }, watch: ['dev/gqlExecutor.js'], // if the watched file doeesn't exist, wait for it instead of restarting - autorestart: false, - log_file: 'dev/logs/gqlExecutor.log', - combine_logs: true + autorestart: false }, { - name: 'Socket Server', - script: 'scripts/runSocketServer.js', + name: 'Embedder', + script: 'scripts/runEmbedder.js', // increase this to test scaling instances: 1, increment_var: 'SERVER_ID', env: { - SERVER_ID: 0 + SERVER_ID: 6 }, - watch: ['dev/web.js'], + watch: ['dev/embedder.js'], // if the watched file doeesn't exist, wait for it instead of restarting - autorestart: false, - log_file: 'dev/logs/web.log', - combine_logs: true + autorestart: false }, { name: 'Dev Server', diff --git a/scripts/generateGraphQLArtifacts.js b/scripts/generateGraphQLArtifacts.js index de2fd452e67..9ca5cd43067 100644 --- a/scripts/generateGraphQLArtifacts.js +++ b/scripts/generateGraphQLArtifacts.js @@ -16,8 +16,11 @@ const generateGraphQLArtifacts = async () => { relayCompiler?.kill() }) }) - - await Promise.all([generate(codegenSchema), runCompiler()]) + console.log('gen graphql artifacts start') + await generate(codegenSchema) + console.log('codegen complete') + await runCompiler() + console.log('relay compiler complete') persistServer.close() } diff --git a/scripts/prod.js b/scripts/prod.js index 65d121446d8..94a7565363b 100644 --- a/scripts/prod.js +++ b/scripts/prod.js @@ -10,15 +10,25 @@ const runChild = (cmd) => { const prod = async (isDeploy, noDeps) => { console.log('🙏🙏🙏 Building Production Server 🙏🙏🙏') - await generateGraphQLArtifacts() - await Promise.all([ - runChild( - `yarn webpack --config ./scripts/webpack/prod.servers.config.js --no-stats --env=noDeps=${noDeps}` - ), - runChild( - `yarn webpack --config ./scripts/webpack/prod.client.config.js --no-stats --env=minimize=${isDeploy}` - ) - ]) + try { + await generateGraphQLArtifacts() + } catch (e) { + console.log('ERR generating artifacts', e) + } + + console.log('starting webpack build') + try { + await Promise.all([ + runChild( + `yarn webpack --config ./scripts/webpack/prod.servers.config.js --no-stats --env=noDeps=${noDeps}` + ), + runChild( + `yarn webpack --config ./scripts/webpack/prod.client.config.js --no-stats --env=minimize=${isDeploy}` + ) + ]) + } catch (e) { + console.log('error webpackifying', e) + } } if (require.main === module) { diff --git a/scripts/runEmbedder.js b/scripts/runEmbedder.js new file mode 100644 index 00000000000..c235888f861 --- /dev/null +++ b/scripts/runEmbedder.js @@ -0,0 +1,13 @@ +/** + Starts up an instance of the stateless Embedder service. + When you modify a server file, the webpack watcher in + {@link buildServers} writes the change to {@link ../dev/embedder}. + Pm2 reloads this file whenever {@link embedder} changes +*/ + +try { + require('../dev/embedder.js') +} catch (e) { + // webpack has not created the file yet + // pm2 will restart this process when the file changes +} diff --git a/scripts/webpack/dev.servers.config.js b/scripts/webpack/dev.servers.config.js index b32e586562c..80591f24b0f 100644 --- a/scripts/webpack/dev.servers.config.js +++ b/scripts/webpack/dev.servers.config.js @@ -7,6 +7,7 @@ const webpack = require('webpack') const PROJECT_ROOT = getProjectRoot() const CLIENT_ROOT = path.join(PROJECT_ROOT, 'packages', 'client') const SERVER_ROOT = path.join(PROJECT_ROOT, 'packages', 'server') +const EMBEDDER_ROOT = path.join(PROJECT_ROOT, 'packages', 'embedder') const GQL_ROOT = path.join(PROJECT_ROOT, 'packages', 'gql-executor') const DOTENV = path.join(PROJECT_ROOT, 'scripts', 'webpack', 'utils', 'dotenv.js') // const CircularDependencyPlugin = require('circular-dependency-plugin') @@ -26,6 +27,7 @@ module.exports = { }, entry: { web: [DOTENV, path.join(SERVER_ROOT, 'server.ts')], + embedder: [DOTENV, path.join(EMBEDDER_ROOT, 'embedder.ts')], gqlExecutor: [DOTENV, path.join(GQL_ROOT, 'gqlExecutor.ts')] }, output: { diff --git a/scripts/webpack/prod.servers.config.js b/scripts/webpack/prod.servers.config.js index 5db575b51e3..52feb216089 100644 --- a/scripts/webpack/prod.servers.config.js +++ b/scripts/webpack/prod.servers.config.js @@ -11,6 +11,7 @@ const cp = require('child_process') const PROJECT_ROOT = getProjectRoot() const CLIENT_ROOT = path.join(PROJECT_ROOT, 'packages', 'client') const SERVER_ROOT = path.join(PROJECT_ROOT, 'packages', 'server') +const EMBEDDER_ROOT = path.join(PROJECT_ROOT, 'packages', 'embedder') const GQL_ROOT = path.join(PROJECT_ROOT, 'packages', 'gql-executor') const DOTENV = path.join(PROJECT_ROOT, 'scripts/webpack/utils/dotenv.js') const distPath = path.join(PROJECT_ROOT, 'dist') @@ -32,6 +33,7 @@ module.exports = (config) => { path.join(PROJECT_ROOT, 'scripts/toolboxSrc/applyEnvVarsToClientAssets.ts'), path.join(SERVER_ROOT, 'server.ts') ], + embedder: [DOTENV, path.join(EMBEDDER_ROOT, 'embedder.ts')], gqlExecutor: [DOTENV, path.join(GQL_ROOT, 'gqlExecutor.ts')], preDeploy: [DOTENV, path.join(PROJECT_ROOT, 'scripts/toolboxSrc/preDeploy.ts')], pushToCDN: [DOTENV, path.join(PROJECT_ROOT, 'scripts/toolboxSrc/pushToCDN.ts')], diff --git a/scripts/webpack/toolbox.config.js b/scripts/webpack/toolbox.config.js index 3edcb77ed56..d63a8f6505c 100644 --- a/scripts/webpack/toolbox.config.js +++ b/scripts/webpack/toolbox.config.js @@ -56,6 +56,26 @@ module.exports = { new webpack.DefinePlugin({ __PRODUCTION__: true }) + // new CircularDependencyPlugin({ + // // `onStart` is called before the cycle detection starts + // onStart({compilation}) { + // console.log('start detecting webpack modules cycles') + // }, + // // `onDetected` is called for each module that is cyclical + // onDetected({module: webpackModuleRecord, paths, compilation}) { + // // `paths` will be an Array of the relative module paths that make up the cycle + // // `module` is the module record that caused the cycle + // compilation.errors.push(new Error(paths.join(' -> '))) + // }, + // // `onEnd` is called before the cycle detection ends + // onEnd({compilation}) { + // console.log('end detecting webpack modules cycles') + // }, + // // set to false to only detect cycles that include an entrypoint + // allowAsyncCycles: false, + // // set to true to detect cycles in node_modules + // cwd: process.cwd() // set the current working directory for displaying module paths + // }) ], module: { rules: [ diff --git a/scripts/webpack/utils/transformRules.js b/scripts/webpack/utils/transformRules.js index 0c44e2ae3e6..e6bc0c9dadf 100644 --- a/scripts/webpack/utils/transformRules.js +++ b/scripts/webpack/utils/transformRules.js @@ -3,6 +3,7 @@ const path = require('path') const transformRules = (projectRoot, isProd) => { const CLIENT_ROOT = path.join(projectRoot, 'packages', 'client') const SERVER_ROOT = path.join(projectRoot, 'packages', 'server') + const EMBEDDER_ROOT = path.join(projectRoot, 'packages', 'embedder') const GQL_ROOT = path.join(projectRoot, 'packages', 'gql-executor') const CHRONOS_ROOT = path.join(projectRoot, 'packages', 'chronos') const TOOLBOX_SRC = path.join(projectRoot, 'scripts', 'toolboxSrc') @@ -48,7 +49,7 @@ const transformRules = (projectRoot, isProd) => { { test: /\.(tsx?|js)$/, // things that don't need babel - include: [SERVER_ROOT, GQL_ROOT, CHRONOS_ROOT, TOOLBOX_SRC], + include: [SERVER_ROOT, EMBEDDER_ROOT, GQL_ROOT, CHRONOS_ROOT, TOOLBOX_SRC], // things that need babel exclude: path.join(SERVER_ROOT, 'email'), use: { diff --git a/yarn.lock b/yarn.lock index 99d793dc367..69cf14bcce1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1317,11 +1317,34 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" -"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.18.6", "@babel/compat-data@^7.22.9": +"@babel/code-frame@^7.16.7", "@babel/code-frame@^7.21.4", "@babel/code-frame@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + +"@babel/compat-data@^7.13.11": + version "7.16.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e" + integrity sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q== + +"@babel/compat-data@^7.18.6": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" + integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== + +"@babel/compat-data@^7.22.9": version "7.22.9" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== +"@babel/compat-data@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" + integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== + "@babel/core@7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.6.tgz#54a107a3c298aee3fe5e1947a6464b9b6faca03d" @@ -1343,7 +1366,91 @@ json5 "^2.2.1" semver "^6.3.0" -"@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.0", "@babel/core@^7.20.12", "@babel/core@^7.22.9": +"@babel/core@^7.11.1": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.2.tgz#87b2fcd7cce9becaa7f5acebdc4f09f3dd19d876" + integrity sha512-A8pri1YJiC5UnkdrWcmfZTJTV85b4UXTAfImGmCfYmax4TR9Cw8sDS0MOk++Gp2mE/BefVJ5nwy5yzqNJbP/DQ== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-compilation-targets" "^7.18.2" + "@babel/helper-module-transforms" "^7.18.0" + "@babel/helpers" "^7.18.2" + "@babel/parser" "^7.18.0" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.2" + "@babel/types" "^7.18.2" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/core@^7.11.6": + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.8.tgz#2a8c7f0f53d60100ba4c32470ba0281c92aa9aa4" + integrity sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.21.4" + "@babel/generator" "^7.21.5" + "@babel/helper-compilation-targets" "^7.21.5" + "@babel/helper-module-transforms" "^7.21.5" + "@babel/helpers" "^7.21.5" + "@babel/parser" "^7.21.8" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.0" + +"@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.0": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.7.tgz#db990f931f6d40cb9b87a0dc7d2adc749f1dcbcf" + integrity sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.16.7" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helpers" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + source-map "^0.5.0" + +"@babel/core@^7.20.12": + version "7.20.12" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" + integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.7" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helpers" "^7.20.7" + "@babel/parser" "^7.20.7" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.12" + "@babel/types" "^7.20.7" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.0" + +"@babel/core@^7.22.9": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.10.tgz#aad442c7bcd1582252cb4576747ace35bc122f35" integrity sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw== @@ -1374,7 +1481,24 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/helper-annotate-as-pure@^7.16.7", "@babel/helper-annotate-as-pure@^7.18.6": +"@babel/generator@^7.16.7", "@babel/generator@^7.18.2", "@babel/generator@^7.20.7", "@babel/generator@^7.21.5", "@babel/generator@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" + integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== + dependencies: + "@babel/types" "^7.23.6" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" + integrity sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== @@ -1400,7 +1524,31 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.18.6": +"@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.18.2", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.21.5": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" + integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== + dependencies: + "@babel/compat-data" "^7.23.5" + "@babel/helper-validator-option" "^7.23.5" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.7.tgz#9c5b34b53a01f2097daf10678d65135c1b9f84ba" + integrity sha512-kIFozAvVfK05DM4EVQYKK+zteWvY85BFdGBRQBytRyY3y+6PX0DkDOn/CZ3lEuczCfrCxEzwt0YtP/87YPTWSw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + +"@babel/helper-create-class-features-plugin@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.6.tgz#6f15f8459f3b523b39e00a99982e2c040871ed72" integrity sha512-YfDzdnoxHGV8CzqHGyCbFvXg5QESPFkXlHtvdCkesLjjVMT2Adxe4FGUR5ChIb3DxSaXO12iIOCWoXdsUVwnqw== @@ -1435,7 +1583,19 @@ resolve "^1.14.2" semver "^6.1.2" -"@babel/helper-environment-visitor@^7.18.6", "@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.22.5": +"@babel/helper-environment-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" + integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-environment-visitor@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.6.tgz#b7eee2b5b9d70602e59d1a6cad7dd24de7ca6cd7" + integrity sha512-8n6gSfn2baOY+qlp+VSzsosjCVGFqWKmDF0cCWOybh52Dw3SEyoWR1KrhMJASjLwIEkkAufZ0xvr+SxLHSpy2Q== + +"@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.22.5": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== @@ -1447,7 +1607,7 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-function-name@^7.18.6", "@babel/helper-function-name@^7.23.0": +"@babel/helper-function-name@^7.16.7", "@babel/helper-function-name@^7.18.6", "@babel/helper-function-name@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== @@ -1462,6 +1622,13 @@ dependencies: "@babel/types" "^7.22.5" +"@babel/helper-member-expression-to-functions@^7.16.7", "@babel/helper-member-expression-to-functions@^7.22.15": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" + integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== + dependencies: + "@babel/types" "^7.23.0" + "@babel/helper-member-expression-to-functions@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.6.tgz#44802d7d602c285e1692db0bad9396d007be2afc" @@ -1469,13 +1636,45 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.22.5": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-imports@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-module-imports@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== dependencies: "@babel/types" "^7.22.5" +"@babel/helper-module-transforms@^7.16.7", "@babel/helper-module-transforms@^7.18.0", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.5": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.22.9": version "7.22.9" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129" @@ -1487,6 +1686,13 @@ "@babel/helper-split-export-declaration" "^7.22.6" "@babel/helper-validator-identifier" "^7.22.5" +"@babel/helper-optimise-call-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz#a34e3560605abbd31a18546bd2aad3e6d9a174f2" + integrity sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" @@ -1494,7 +1700,29 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": +"@babel/helper-optimise-call-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" + integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" + integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== + +"@babel/helper-plugin-utils@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.6.tgz#9448974dd4fb1d80fefe72e8a0af37809cd30d6d" + integrity sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg== + +"@babel/helper-plugin-utils@^7.20.2": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56" + integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg== + +"@babel/helper-plugin-utils@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== @@ -1509,6 +1737,15 @@ "@babel/helper-wrap-function" "^7.18.6" "@babel/types" "^7.18.6" +"@babel/helper-replace-supers@^7.16.7": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793" + integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.6.tgz#efedf51cfccea7b7b8c0f00002ab317e7abfe420" @@ -1520,7 +1757,7 @@ "@babel/traverse" "^7.18.6" "@babel/types" "^7.18.6" -"@babel/helper-simple-access@^7.18.6", "@babel/helper-simple-access@^7.22.5": +"@babel/helper-simple-access@^7.16.7", "@babel/helper-simple-access@^7.18.6", "@babel/helper-simple-access@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== @@ -1534,19 +1771,39 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-split-export-declaration@^7.18.6", "@babel/helper-split-export-declaration@^7.22.6": +"@babel/helper-split-export-declaration@^7.16.7", "@babel/helper-split-export-declaration@^7.18.6", "@babel/helper-split-export-declaration@^7.22.6": version "7.22.6" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== dependencies: "@babel/types" "^7.22.5" +"@babel/helper-string-parser@^7.19.4", "@babel/helper-string-parser@^7.21.5", "@babel/helper-string-parser@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" + integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== + "@babel/helper-string-parser@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.22.5": +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/helper-validator-identifier@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" + integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== + +"@babel/helper-validator-identifier@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.22.5": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== @@ -1556,6 +1813,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== +"@babel/helper-validator-option@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" + integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== + "@babel/helper-wrap-function@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.18.6.tgz#ec44ea4ad9d8988b90c3e465ba2382f4de81a073" @@ -1566,6 +1828,15 @@ "@babel/traverse" "^7.18.6" "@babel/types" "^7.18.6" +"@babel/helpers@^7.16.7", "@babel/helpers@^7.18.2", "@babel/helpers@^7.20.7", "@babel/helpers@^7.21.5": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d" + integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ== + dependencies: + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" + "@babel/helpers@^7.18.6", "@babel/helpers@^7.22.10": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.10.tgz#ae6005c539dfbcb5cd71fb51bfc8a52ba63bc37a" @@ -1584,11 +1855,55 @@ chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.8", "@babel/parser@^7.18.6", "@babel/parser@^7.20.15", "@babel/parser@^7.22.10", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0": +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.7.tgz#d372dda9c89fcec340a82630a9f533f2fe15877e" + integrity sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA== + +"@babel/parser@^7.16.8", "@babel/parser@^7.18.6": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.8.tgz#822146080ac9c62dac0823bb3489622e0bc1cbdf" + integrity sha512-RSKRfYX20dyH+elbJK2uqAkVyucL+xXzhqlMD5/ZXx+dAAwpyB7HsvnHe/ZUGOF+xLr5Wx9/JoXVTj6BQE2/oA== + +"@babel/parser@^7.18.0": + version "7.18.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.4.tgz#6774231779dd700e0af29f6ad8d479582d7ce5ef" + integrity sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow== + +"@babel/parser@^7.20.15", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== +"@babel/parser@^7.20.7": + version "7.20.15" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89" + integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg== + +"@babel/parser@^7.21.8": + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" + integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== + +"@babel/parser@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55" + integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ== + +"@babel/parser@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" + integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -2002,7 +2317,17 @@ "@babel/helper-plugin-utils" "^7.18.6" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.13.8", "@babel/plugin-transform-modules-commonjs@^7.18.6": +"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.13.8": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.7.tgz#fd119e6a433c527d368425b45df361e1e95d3c1a" + integrity sha512-h2RP2kE7He1ZWKyAlanMZrAbdv+Acw1pA8dQZhE025WJZE2z0xzFADAinXA9fxd5bn7JnM+SdOGcndGx1ARs9w== + dependencies: + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-simple-access" "^7.16.7" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-commonjs@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.18.6.tgz#afd243afba166cca69892e24a8fd8c9f2ca87883" integrity sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q== @@ -2101,7 +2426,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-shorthand-properties@^7.0.0", "@babel/plugin-transform-shorthand-properties@^7.18.6": +"@babel/plugin-transform-shorthand-properties@^7.0.0": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz#e8549ae4afcf8382f711794c0c7b6b934c5fbd2a" + integrity sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-shorthand-properties@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz#6d6df7983d67b195289be24909e3f12a8f664dc9" integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== @@ -2289,14 +2621,83 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.21.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" + integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" + integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.10.5": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" + integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.11.2": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" + integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.13.10": + version "7.20.13" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" + integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== + dependencies: + regenerator-runtime "^0.13.11" + +"@babel/runtime@^7.17.2": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" + integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.21.0": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200" integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== dependencies: regenerator-runtime "^0.13.11" -"@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.3.3": +"@babel/template@^7.16.7", "@babel/template@^7.3.3": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/template@^7.18.10", "@babel/template@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" + integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + +"@babel/template@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31" + integrity sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/template@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== @@ -2305,6 +2706,15 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" +"@babel/template@^7.22.5", "@babel/template@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" + integrity sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" + "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.18.6", "@babel/traverse@^7.22.10", "@babel/traverse@^7.7.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" @@ -2321,7 +2731,82 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.18.13", "@babel/types@^7.18.6", "@babel/types@^7.22.10", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": +"@babel/traverse@^7.16.7", "@babel/traverse@^7.18.2", "@babel/traverse@^7.20.12", "@babel/traverse@^7.21.5", "@babel/traverse@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" + integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.16.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.7.tgz#4ed19d51f840ed4bd5645be6ce40775fecf03159" + integrity sha512-E8HuV7FO9qLpx6OtoGfUQ2cjIYnbFwvZWYBS+87EwtdMvmUPJSwykpovFB+8insbpF0uJcpr8KMUi64XZntZcg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + +"@babel/types@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1" + integrity sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + +"@babel/types@^7.18.13", "@babel/types@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" + integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + +"@babel/types@^7.18.2": + version "7.18.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.4.tgz#27eae9b9fd18e9dccc3f9d6ad051336f307be354" + integrity sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + +"@babel/types@^7.18.6": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.8.tgz#c5af199951bf41ba4a6a9a6d0d8ad722b30cd42f" + integrity sha512-qwpdsmraq0aJ3osLJRApsc2ouSJCdnMeZwB0DhbtHAtRpZNZCdlbRnHIgcRKzdE1g0iOGg644fzjOBcdOz9cPw== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + +"@babel/types@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6" + integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q== + dependencies: + "@babel/helper-string-parser" "^7.21.5" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + +"@babel/types@^7.22.10", "@babel/types@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03" + integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + to-fast-properties "^2.0.0" + +"@babel/types@^7.22.15", "@babel/types@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== @@ -2330,6 +2815,15 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.23.6", "@babel/types@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" + integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -7968,13 +8462,22 @@ agent-base@^7.0.2, agent-base@^7.1.0: dependencies: debug "^4.3.4" -agentkeepalive@^4.1.3, agentkeepalive@^4.2.1: +agentkeepalive@^4.1.3: version "4.5.0" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== dependencies: humanize-ms "^1.2.1" +agentkeepalive@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" + integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -8408,7 +8911,16 @@ axios@0.21.4, axios@^0.21.0, axios@^0.21.4: dependencies: follow-redirects "^1.14.0" -axios@^1.0.0, axios@^1.3.3: +axios@^1.0.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.2.tgz#7ac517f0fa3ec46e0e636223fd973713a09c72b3" + integrity sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +axios@^1.3.3: version "1.6.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102" integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg== @@ -8790,6 +9302,16 @@ browserslist@^4.14.5, browserslist@^4.21.1, browserslist@^4.21.4, browserslist@^ node-releases "^2.0.13" update-browserslist-db "^1.0.11" +browserslist@^4.22.2: + version "4.22.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.3.tgz#299d11b7e947a6b843981392721169e27d60c5a6" + integrity sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A== + dependencies: + caniuse-lite "^1.0.30001580" + electron-to-chromium "^1.4.648" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -9011,10 +9533,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001517, caniuse-lite@~1.0.0: - version "1.0.30001587" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz#a0bce920155fa56a1885a69c74e1163fc34b4881" - integrity sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA== +caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001580, caniuse-lite@~1.0.0: + version "1.0.30001571" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001571.tgz#4182e93d696ff42930f4af7eba515ddeb57917ac" + integrity sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ== capital-case@^1.0.4: version "1.0.4" @@ -9075,7 +9597,7 @@ chalk@4.1.2, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1. ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^2.4.2: +chalk@^2.3.2, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -9177,7 +9699,22 @@ cheerio@^1.0.0-rc.10: parse5-htmlparser2-tree-adapter "^6.0.1" tslib "^2.2.0" -chokidar@^3.3.1, chokidar@^3.4.0, chokidar@^3.5.1, chokidar@^3.5.3: +chokidar@^3.3.1, chokidar@^3.4.0, chokidar@^3.5.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" + integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -10307,10 +10844,10 @@ depd@2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -depd@~1.1.2: +depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== dependency-graph@^0.11.0: version "0.11.0" @@ -10394,6 +10931,11 @@ diff-sequences@^29.4.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== +diff@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -10588,6 +11130,7 @@ draft-js-utils@^1.4.0: "draft-js@https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6": version "0.10.5" + uid "025fddba56f21aaf3383aee778e0b17025c9a7bc" resolved "https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6#025fddba56f21aaf3383aee778e0b17025c9a7bc" dependencies: fbjs "^0.8.15" @@ -10658,6 +11201,11 @@ electron-to-chromium@^1.4.477: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.490.tgz#d99286f6e915667fa18ea4554def1aa60eb4d5f1" integrity sha512-6s7NVJz+sATdYnIwhdshx/N/9O6rvMxmhVoDSDFdj6iA45gHR8EQje70+RYsF4GeB+k0IeNSBnP7yG9ZXJFr7A== +electron-to-chromium@^1.4.648: + version "1.4.660" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.660.tgz#80be71d08c1224980e645904ab9155f3fa54a1ea" + integrity sha512-1BqvQG0BBQrAA7FVL2EMrb5A1sVyXF3auwJneXjGWa1TpN+g0C4KbUsYWePz6OZ0mXZfXGy+RmQDELJWwE8v/Q== + email-addresses@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-3.1.0.tgz#cabf7e085cbdb63008a70319a74e6136188812fb" @@ -11640,7 +12188,7 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@^11.0.0, fs-extra@^11.1.0: +fs-extra@^11.0.0: version "11.1.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== @@ -11649,6 +12197,15 @@ fs-extra@^11.0.0, fs-extra@^11.1.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.0.tgz#5784b102104433bb0e090f48bfc4a30742c357ed" + integrity sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -11715,6 +12272,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" @@ -11900,6 +12462,17 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +git-diff@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/git-diff/-/git-diff-2.0.6.tgz#4a8ece670d64d1f9f4e68191ad8b1013900f6c1e" + integrity sha512-/Iu4prUrydE3Pb3lCBMbcSNIf81tgGt0W1ZwknnyF62t3tHmtiJTRj0f+1ZIhp3+Rh0ktz1pJVoa7ZXUCskivA== + dependencies: + chalk "^2.3.2" + diff "^3.5.0" + loglevel "^1.6.1" + shelljs "^0.8.1" + shelljs.exec "^1.1.7" + git-node-fs@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/git-node-fs/-/git-node-fs-1.0.0.tgz#49b215e242ebe43aa4c7561bbba499521752080f" @@ -12350,6 +12923,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + hdr-histogram-js@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz#0b860534655722b6e3f3e7dca7b78867cf43dcb5" @@ -12852,6 +13432,11 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" @@ -12968,6 +13553,13 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.8.1, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" @@ -14172,20 +14764,21 @@ koalas@^1.0.2: resolved "https://registry.yarnpkg.com/koalas/-/koalas-1.0.2.tgz#318433f074235db78fae5661a02a8ca53ee295cd" integrity sha1-MYQz8HQjXbePrlZhoCqMpT7ilc0= -kysely-codegen@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/kysely-codegen/-/kysely-codegen-0.10.0.tgz#ccc4a1d9c1a2f334a35d820554880251f2386524" - integrity sha512-EV2v9yr9N9WrUPrURvFl5FPTtz39npsi1p1tLpJwEcFeOAKAPJruBpAASvnZWdqu5Pw/aaTssStE3qcI2sfxMQ== +kysely-codegen@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/kysely-codegen/-/kysely-codegen-0.11.0.tgz#7506955c4c09201b571d528b42ffec8a1869160b" + integrity sha512-8aklzXygjANshk5BoGSQ0BWukKIoPL4/k1iFWyteGUQ/VtB1GlyrELBZv1GglydjLGECSSVDpsOgEXyWQmuksg== dependencies: chalk "4.1.2" dotenv "^16.0.3" + git-diff "^2.0.6" micromatch "^4.0.5" minimist "^1.2.8" -kysely@^0.26.3: - version "0.26.3" - resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.26.3.tgz#45fdd0153d8c9418b0ea9a6f05ed46b95ed27678" - integrity sha512-yWSgGi9bY13b/W06DD2OCDDHQmq1kwTGYlQ4wpZkMOJqMGCstVCFIvxCCVG4KfY1/3G0MhDAcZsip/Lw8/vJWw== +kysely@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.27.2.tgz#b289ce5e561064ec613a17149b7155783d2b36de" + integrity sha512-DmRvEfiR/NLpgsTbSxma2ldekhsdcd65+MNiKXyd/qj7w7X5e3cLkXxcj+MypsRDjPhHQ/CD5u3Eq1sBYzX0bw== launch-editor@^2.6.0: version "2.6.0" @@ -14628,6 +15221,11 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" +loglevel@^1.6.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.1.tgz#d63976ac9bcd03c7c873116d41c2a85bafff1be7" + integrity sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg== + long@^5.0.0: version "5.2.3" resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" @@ -15512,6 +16110,11 @@ node-releases@^2.0.13: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + node-rsa@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.1.1.tgz#efd9ad382097782f506153398496f79e4464434d" @@ -17853,6 +18456,13 @@ recast@^0.21.0: source-map "~0.6.1" tslib "^2.0.1" +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + rechoir@^0.7.0: version "0.7.1" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" @@ -17895,6 +18505,13 @@ redis-parser@^3.0.0: dependencies: redis-errors "^1.0.0" +redlock@^5.0.0-beta.2: + version "5.0.0-beta.2" + resolved "https://registry.yarnpkg.com/redlock/-/redlock-5.0.0-beta.2.tgz#a629c07e07d001c0fdd9f2efa614144c4416fe44" + integrity sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw== + dependencies: + node-abort-controller "^3.0.1" + redux@^4.0.0, redux@^4.0.4: version "4.1.2" resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" @@ -17924,11 +18541,16 @@ regenerator-runtime@^0.12.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== -regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.5: +regenerator-runtime@^0.13.11: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.5: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + regenerator-transform@^0.15.0: version "0.15.0" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" @@ -18133,6 +18755,15 @@ resolve@^1.0.0, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14. path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.1.6: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.3: version "2.0.0-next.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" @@ -18560,6 +19191,20 @@ shell-quote@^1.7.3, shell-quote@^1.8.0: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== +shelljs.exec@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/shelljs.exec/-/shelljs.exec-1.1.8.tgz#6f3c8dd017cb96d2dea82e712b758eab4fc2f68c" + integrity sha512-vFILCw+lzUtiwBAHV8/Ex8JsFjelFMdhONIsgKNLgTzeRckp2AOYRQtHJE/9LhNvdMmE27AGtzWx0+DHpwIwSw== + +shelljs@^0.8.1: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + shimmer@^1.1.0, shimmer@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" @@ -18831,10 +19476,10 @@ source-map-support@0.5.21, source-map-support@^0.5.12, source-map-support@^0.5.1 buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.5.7: +source-map@^0.5.0, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" @@ -19127,7 +19772,16 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: +string-width@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.0.tgz#5ab00980cfb29f43e736b113a120a73a0fb569d3" + integrity sha512-7x54QnN21P+XL/v8SuNKvfgsUre6PXpN7mc77N3HlZv+f1SBRGmjxtOud2Z6FZ8DmdkD/IdjCaf9XXbnqmTZGQ== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== @@ -20244,6 +20898,14 @@ update-browserslist-db@^1.0.11: escalade "^3.1.1" picocolors "^1.0.0" +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + upper-case-first@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324" @@ -21239,7 +21901,7 @@ yargs-parser@20.2.4: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== -yargs-parser@21.1.1, yargs-parser@^21.0.1, yargs-parser@^21.1.1: +yargs-parser@21.1.1, yargs-parser@^21.0.0, yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== @@ -21287,7 +21949,20 @@ yargs@^16.2.0, yargs@~16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.0, yargs@^17.0.1, yargs@^17.3.1, yargs@^17.6.2, yargs@^17.7.1, yargs@^17.7.2: +yargs@^17.0.0, yargs@^17.0.1: + version "17.3.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" + integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + +yargs@^17.3.1, yargs@^17.6.2, yargs@^17.7.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== From ac2807a1ea132ec7e3828d0c72859bd7682718e9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 11:44:32 +0100 Subject: [PATCH 03/19] chore(release): Test v7.19.5 (#9478) * chore(env vars): Stripe vars moved to the Integrations section (#9427) * chore: fix misleading `isLead` field name on `Team` (#9413) * chore: fix misleading `isLead` field name on `Team` The field indicates whether the viewer is the lead, but when used in a query for a different user, the result could be read wrong. * Fix Team.isLead dependencies * feat: remove team template limit (#9424) * update error message and increase template limit * remove max team template limits * remove canClone prop from CloneTemplate * remove unused threshold * remove unused threshold * feat: Add Google calendar meeting series for recurrence (#9380) * feat: Add recurrence to GCal events * Fun with timezones * fix: Increase the number of projects fetched per request from Atlassian (#9435) We ran into timeouts in `getAllProjects`, presumably because we're doing too many roundtrips. As a quick fix, increse the number of projects fetched per request from 50 to 500. * chore(deps): bump ip from 1.1.8 to 1.1.9 (#9442) Bumps [ip](https://github.com/indutny/node-ip) from 1.1.8 to 1.1.9. - [Commits](https://github.com/indutny/node-ip/compare/v1.1.8...v1.1.9) --- updated-dependencies: - dependency-name: ip dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(release): release v7.17.0 (#9428) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * feat(standalone-deployment): Standalone host deployment improved and documented (#9445) * Docker compose stack improved * Remove unused containers from docker-compse and add useful comment on .env.example about PGSSLMODE * Docker compose profiles added. Documentation extended on how to use the profiles to manage the stack. * README fixed as docker compose up and down commands were not working * Typo fixed and docker-compose command replaced by docker compose * feat: support env-defined saml issuer for PPMIs (#9455) * feat: support env-defined saml issuer for PPMIs Signed-off-by: Matt Krick * feat: support single SAML for entire tenant Signed-off-by: Matt Krick --------- Signed-off-by: Matt Krick * chore: Associate logs with traces (#9444) * chore: Associate logs with traces Add trace information to log output for server side log statements. This does not include logging from code exclusively used for debugging, deploying or development. * Actually add the logger * Fix DD_LOGS_INJECTION check * chore(release): release v7.18.0 (#9450) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * chore: no force-push to prod (#9401) Signed-off-by: Matt Krick * chore(release): release v7.18.1 (#9459) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * feat: embedder service (#9417) * feat: add embedder service --------- Signed-off-by: Matt Krick Co-authored-by: Matt Krick * merge production to avoid force push (#9461) Signed-off-by: Matt Krick * chore(release): release v7.19.0 (#9460) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: checkout prod before merging it (#9463) Signed-off-by: Matt Krick * chore(release): release v7.19.1 (#9464) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: mrege origin/production strategy (#9465) Signed-off-by: Matt Krick * chore(release): release v7.19.2 (#9466) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: force push 5 (#9467) Signed-off-by: Matt Krick * chore(release): release v7.19.3 (#9468) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: limit invites from spammers (#9416) * fix: limit invites from spammers * update where we check pending emails * check total plus pending invites * use invitees instead of pending * fix: Fetch Jira projects in parallel (#9456) Previously we tried to fetch more projects per page, but Jira only ever returns 50 max. Instead once we know how many projects there are after fetching the first page, we fetch all remaining pages in parallel. * fix: replace lone surrogates in draft-js content (#9415) * fix: replace lone surrogates in draft-js content Signed-off-by: Matt Krick * fix typo Signed-off-by: Matt Krick * fix: eslint errors Signed-off-by: Matt Krick --------- Signed-off-by: Matt Krick * chore(deps): bump es5-ext from 0.10.62 to 0.10.64 (#9457) Bumps [es5-ext](https://github.com/medikoo/es5-ext) from 0.10.62 to 0.10.64. - [Release notes](https://github.com/medikoo/es5-ext/releases) - [Changelog](https://github.com/medikoo/es5-ext/blob/main/CHANGELOG.md) - [Commits](https://github.com/medikoo/es5-ext/compare/v0.10.62...v0.10.64) --- updated-dependencies: - dependency-name: es5-ext dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: packages/server/package.json to reduce vulnerabilities (#9434) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-UNDICI-6252336 Co-authored-by: snyk-bot * fix: packages/server/package.json to reduce vulnerabilities (#9392) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-NODEMAILER-6219989 Co-authored-by: snyk-bot * fix: packages/server/package.json to reduce vulnerabilities (#9298) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-FOLLOWREDIRECTS-6141137 Co-authored-by: snyk-bot * chore(deps): bump follow-redirects from 1.14.8 to 1.15.4 (#9312) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.8 to 1.15.4. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.8...v1.15.4) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: Matt Krick Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matt Krick * chore: add upload to GCS step in ironbank (#9471) * add upload to GCS step in ironbank * update workflow name * chore(release): release v7.19.4 (#9470) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: Fix seasonal templates for leap years (#9476) * fix: Fix seasonal templates for leap years It would produce invalid dates on February 29th. * Master was not clean * chore(release): release v7.19.5 (#9477) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Signed-off-by: Matt Krick Co-authored-by: Rafa <101704572+rafaelromcar-parabol@users.noreply.github.com> Co-authored-by: Georg Bremer Co-authored-by: Nick O'Ferrall Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> Co-authored-by: Matt Krick Co-authored-by: Jordan Husney Co-authored-by: adaniels-parabol <71724289+adaniels-parabol@users.noreply.github.com> Co-authored-by: snyk-bot Co-authored-by: Dale Bumblis <135627447+dbumblis-parabol@users.noreply.github.com> Co-authored-by: github-actions --- .eslintrc.js | 1 + .github/workflows/ironbank.yml | 10 +- .release-please-manifest.json | 2 +- CHANGELOG.md | 26 ++ package.json | 9 +- packages/chronos/package.json | 6 +- packages/client/package.json | 4 +- packages/client/utils/AtlassianManager.ts | 2 +- .../draftjs/extractTextFromDraftString.ts | 3 +- packages/gql-executor/package.json | 10 +- packages/integration-tests/package.json | 7 +- .../server/dataloader/customLoaderMakers.ts | 4 +- .../mutations/helpers/inviteToTeamHelper.ts | 15 + packages/server/package.json | 12 +- packages/server/tsconfig.json | 15 +- .../server/utils/AtlassianServerManager.ts | 54 +++- yarn.lock | 300 ++++++++++++------ 17 files changed, 330 insertions(+), 150 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 111e259c651..1962c0f1b60 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,6 +20,7 @@ module.exports = { '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/explicit-member-accessibility': 'off', '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/no-duplicate-enum-values': 'off', '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-parameter-properties': 'off', diff --git a/.github/workflows/ironbank.yml b/.github/workflows/ironbank.yml index b6a15acd539..e9782493f43 100644 --- a/.github/workflows/ironbank.yml +++ b/.github/workflows/ironbank.yml @@ -1,4 +1,4 @@ -name: Ironbank S3 Upload +name: Ironbank Image Upload on: workflow_dispatch: @@ -64,3 +64,11 @@ jobs: - name: Upload to S3 run: | aws s3 cp ${{ github.event.inputs.version_number }}.zip s3://ironbank-proving-ground-action-files.parabol.co/${{ github.event.inputs.version_number }}.zip + + - name: Upload to GCS + uses: actions-hub/gcloud@master + env: + CLOUDSDK_AUTH_ACCESS_TOKEN: "${{ steps.auth.outputs.access_token }}" + with: + args: storage cp ${{ github.event.inputs.version_number }}.zip gs://ironbank-proving-ground/${{ github.event.inputs.version_number }}.zip + cli: gcloud diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 517aeb7a63e..6e9dcf42d00 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.19.3" + ".": "7.19.5" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 579182dc3f5..080f2ea136c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.19.5](https://github.com/ParabolInc/parabol/compare/v7.19.4...v7.19.5) (2024-02-29) + + +### Fixed + +* Fix seasonal templates for leap years ([#9476](https://github.com/ParabolInc/parabol/issues/9476)) ([419d104](https://github.com/ParabolInc/parabol/commit/419d104757d905c468d6a72ce607430d01f3b97f)) + +## [7.19.4](https://github.com/ParabolInc/parabol/compare/v7.19.3...v7.19.4) (2024-02-28) + + +### Fixed + +* Fetch Jira projects in parallel ([#9456](https://github.com/ParabolInc/parabol/issues/9456)) ([9cec00a](https://github.com/ParabolInc/parabol/commit/9cec00a5fd0b46c73ebdde27e6d966b485216132)) +* limit invites from spammers ([#9416](https://github.com/ParabolInc/parabol/issues/9416)) ([5b9526c](https://github.com/ParabolInc/parabol/commit/5b9526c092f7f8675ad2a442da4440e2507cbdcc)) +* packages/server/package.json to reduce vulnerabilities ([#9298](https://github.com/ParabolInc/parabol/issues/9298)) ([fd75d3f](https://github.com/ParabolInc/parabol/commit/fd75d3f2a907888bb461d55ac945d9449071a414)) +* packages/server/package.json to reduce vulnerabilities ([#9392](https://github.com/ParabolInc/parabol/issues/9392)) ([fd833f5](https://github.com/ParabolInc/parabol/commit/fd833f541ef7f915b40331c9d12e94243c8fa24f)) +* packages/server/package.json to reduce vulnerabilities ([#9434](https://github.com/ParabolInc/parabol/issues/9434)) ([1e0075e](https://github.com/ParabolInc/parabol/commit/1e0075e843ce3cf52966a0b77293d72f1d9c60b9)) +* replace lone surrogates in draft-js content ([#9415](https://github.com/ParabolInc/parabol/issues/9415)) ([00092ec](https://github.com/ParabolInc/parabol/commit/00092ec55659d1441e9566d501940dcc6fcf07f4)) + + +### Changed + +* add upload to GCS step in ironbank ([#9471](https://github.com/ParabolInc/parabol/issues/9471)) ([7bfec91](https://github.com/ParabolInc/parabol/commit/7bfec9188a42b38eb69930fdd86e6fb39249ed7e)) +* **deps:** bump es5-ext from 0.10.62 to 0.10.64 ([#9457](https://github.com/ParabolInc/parabol/issues/9457)) ([92f0be9](https://github.com/ParabolInc/parabol/commit/92f0be917d4bd182bc6ea249f5dc40c05b98320a)) +* **deps:** bump follow-redirects from 1.14.8 to 1.15.4 ([#9312](https://github.com/ParabolInc/parabol/issues/9312)) ([9441b27](https://github.com/ParabolInc/parabol/commit/9441b2727deefb7e27e4015f37d64ff933415c8d)) + ## [7.19.3](https://github.com/ParabolInc/parabol/compare/v7.19.2...v7.19.3) (2024-02-28) diff --git a/package.json b/package.json index cc2eb303c5d..ee9f994f9d9 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.19.3", + "version": "7.19.5", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -52,7 +52,7 @@ "test:server": "yarn workspace parabol-server test" }, "resolutions": { - "typescript": "4.9.5", + "typescript": "^5.3.3", "hoist-non-react-statics": "^3.3.0", "@types/react": "16.9.11", "@types/react-dom": "16.9.4", @@ -92,8 +92,8 @@ "@types/dotenv": "^6.1.1", "@types/jscodeshift": "^0.11.3", "@types/lodash.toarray": "^4.4.7", - "@typescript-eslint/eslint-plugin": "5.17.0", - "@typescript-eslint/parser": "5.17.0", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", "autoprefixer": "^10.4.13", "babel-loader": "^9.1.2", "concurrently": "^8.0.1", @@ -123,6 +123,7 @@ "tailwindcss": "^3.2.7", "terser-webpack-plugin": "^5.3.9", "ts-loader": "9.2.6", + "typescript": "^5.3.3", "vscode-apollo-relay": "^1.5.0", "webpack": "^5.89.0", "webpack-cli": "4.9.1", diff --git a/packages/chronos/package.json b/packages/chronos/package.json index ff73a275826..f9c5675491e 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.19.3", + "version": "7.19.5", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -21,10 +21,10 @@ }, "devDependencies": { "@types/cron": "^2.0.1", - "@types/node": "^16.11.62" + "@types/node": "^20.11.17" }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.19.3" + "parabol-server": "7.19.5" } } diff --git a/packages/client/package.json b/packages/client/package.json index 364dbda0932..b5e0427873c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.19.3", + "version": "7.19.5", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -60,7 +60,7 @@ "prettier": "^2.8.8", "react-refresh": "^0.9.0", "strict-event-emitter-types": "^2.0.0", - "typescript": "4.9.5", + "typescript": "^5.3.3", "webpack-dev-server": "^4.15.1" }, "dependencies": { diff --git a/packages/client/utils/AtlassianManager.ts b/packages/client/utils/AtlassianManager.ts index c501b9bc768..5726a0e5636 100644 --- a/packages/client/utils/AtlassianManager.ts +++ b/packages/client/utils/AtlassianManager.ts @@ -40,7 +40,7 @@ export type JiraPermissionScope = export class RateLimitError { retryAt: Date - name: 'RateLimitError' = 'RateLimitError' + name = 'RateLimitError' as const message: string constructor(message: string, retryAt: Date) { diff --git a/packages/client/utils/draftjs/extractTextFromDraftString.ts b/packages/client/utils/draftjs/extractTextFromDraftString.ts index a7ac9f22b4f..95e03014184 100644 --- a/packages/client/utils/draftjs/extractTextFromDraftString.ts +++ b/packages/client/utils/draftjs/extractTextFromDraftString.ts @@ -2,7 +2,8 @@ import {RawDraftContentState} from 'draft-js' const extractTextFromDraftString = (content: string) => { const parsedContent = JSON.parse(content) as RawDraftContentState - const textBlocks = parsedContent.blocks.map(({text}) => text) + // toWellFormed replaces lone surrogates with replacement char (e.g. emoji that only has its first code point) + const textBlocks = parsedContent.blocks.map(({text}) => (text as any).toWellFormed()) return textBlocks.join('\n') } diff --git a/packages/gql-executor/package.json b/packages/gql-executor/package.json index 84dbff4bdfb..0e97990948c 100644 --- a/packages/gql-executor/package.json +++ b/packages/gql-executor/package.json @@ -1,6 +1,6 @@ { "name": "gql-executor", - "version": "7.19.3", + "version": "7.19.5", "description": "A Stateless GraphQL Executor", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/gqlExecutor#readme", @@ -18,17 +18,17 @@ "devDependencies": { "@babel/cli": "7.18.6", "@babel/core": "7.18.6", - "@types/node": "^16.11.62", + "@types/node": "^20.11.17", "babel-plugin-inline-import": "^3.0.0", "chokidar": "^3.3.1", "sucrase": "^3.32.0", "ts-node-dev": "^1.0.0-pre.44", - "typescript": "4.9.5" + "typescript": "^5.3.3" }, "dependencies": { "dd-trace": "^4.2.0", - "parabol-client": "7.19.3", - "parabol-server": "7.19.3", + "parabol-client": "7.19.5", + "parabol-server": "7.19.5", "undici": "^5.26.2" } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index f105f56d7da..1b7a9030731 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -2,7 +2,7 @@ "name": "integration-tests", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.19.3", + "version": "7.19.5", "description": "", "main": "index.js", "scripts": { @@ -11,13 +11,10 @@ }, "devDependencies": { "@playwright/test": "^1.34.3", - "@types/node": "^16.11.62", - "@typescript-eslint/eslint-plugin": "^5.17.0", - "@typescript-eslint/parser": "^5.17.0", "eslint": "^8.8.0", "eslint-config-prettier": "^8.5.0", "lint-staged": "^12.3.3", "ts-app-env": "^1.4.2", - "typescript": "^4.5.5" + "typescript": "^5.3.3" } } diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index 8bd64c6250b..ac7e1ea6ec7 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -481,8 +481,8 @@ export const meetingTemplatesByOrgId = (parent: RootDataLoader) => { .where(({or, eb}) => or([ eb('hideStartingAt', 'is', null), - sql`make_date(2020 , extract(month from current_date)::integer, extract(day from current_date)::integer) between "hideEndingAt" and "hideStartingAt"`, - sql`make_date(2019 , extract(month from current_date)::integer, extract(day from current_date)::integer) between "hideEndingAt" and "hideStartingAt"` + sql`DATE '2020-01-01' + EXTRACT(DOY FROM CURRENT_DATE)::INTEGER - 1 between "hideEndingAt" and "hideStartingAt"`, + sql`DATE '2019-01-01' + EXTRACT(DOY FROM CURRENT_DATE)::INTEGER - 1 between "hideEndingAt" and "hideStartingAt"` ]) ) .orderBy('createdAt', 'desc') diff --git a/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts b/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts index c78036add14..053ca0f381f 100644 --- a/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts +++ b/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts @@ -37,6 +37,21 @@ const inviteToTeamHelper = async ( const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} + const [total, pending] = await Promise.all([ + r.table('TeamInvitation').getAll(teamId, {index: 'teamId'}).count().run(), + r + .table('TeamInvitation') + .getAll(teamId, {index: 'teamId'}) + .filter({acceptedAt: null}) + .count() + .run() + ]) + const accepted = total - pending + // if no one has accepted one of their 100+ invites, don't trust them + if (accepted === 0 && total + invitees.length >= 100) { + return standardError(new Error('Exceeded unaccepted invitation limit'), {userId: viewerId}) + } + const untrustedDomains = ['tempmail.cn', 'qq.com'] const filteredInvitees = invitees.filter( (invitee) => !untrustedDomains.includes(getDomainFromEmail(invitee).toLowerCase()) diff --git a/packages/server/package.json b/packages/server/package.json index 625dc427b3f..5dc86873eb0 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.19.3", + "version": "7.19.5", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -39,7 +39,7 @@ "@types/jsonwebtoken": "^8.3.0", "@types/mime-types": "^2.1.0", "@types/ms": "^0.7.30", - "@types/node": "^16.11.62", + "@types/node": "^20.11.17", "@types/nodemailer": "^6.4.14", "@types/relay-runtime": "^14.1.9", "@types/sharp": "^0.32.0", @@ -68,7 +68,7 @@ "sucrase": "^3.32.0", "ts-jest": "^29.1.0", "ts-node": "^8.6.2", - "typescript": "4.9.5", + "typescript": "^5.3.3", "url-loader": "4.1.1", "vscode-apollo-relay": "^1.5.0", "webpack-bundle-analyzer": "4.3.0", @@ -87,7 +87,7 @@ "@sentry/integrations": "^7.74.1", "@sentry/node": "^7.74.1", "adaptivecards": "^2.10.0", - "analytics-node": "^5.0.0", + "analytics-node": "^6.0.0", "api": "^5.0.7", "base64url": "^3.0.1", "bcryptjs": "^2.4.3", @@ -124,7 +124,7 @@ "oauth-1.0a": "^2.2.6", "openai": "^4.24.1", "oy-vey": "^0.11.0", - "parabol-client": "7.19.3", + "parabol-client": "7.19.5", "pg": "^8.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -137,6 +137,6 @@ "stripe": "^9.13.0", "tslib": "^2.4.0", "uWebSockets.js": "uNetworking/uWebSockets.js#v20.34.0", - "undici": "^5.26.2" + "undici": "^5.28.3" } } diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 4a095c384e0..a1a9c3130df 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -8,15 +8,8 @@ "parabol-client/*": ["client/*"], "~/*": ["client/*"] }, - "lib": [ - "esnext", - "dom" - ], - "types": [ - "node", - "jest", - "jest-extended" - ] + "lib": ["esnext", "dom"], + "types": ["node", "jest", "jest-extended"] }, "exclude": [ @@ -31,9 +24,7 @@ "server.ts", "../client/modules/email/components/SummaryEmail/MeetingSummaryEmail/MeetingSummaryEmail.tsx" ], - "include": [ - "graphql/**/*.ts", - ] + "include": ["graphql/**/*.ts"] // if "include" or "files" is added, even if they are empty arrays, then strictNullChecks breaks // repro: https://www.typescriptlang.org/play?strictFunctionTypes=false&strictPropertyInitialization=false&strictBindCallApply=false&noImplicitThis=false&noImplicitReturns=false&alwaysStrict=false&declaration=false&experimentalDecorators=false&emitDecoratorMetadata=false&target=6&ts=3.5.1#code/C4TwDgpgBA8gRgKygXigbwFBSgWwIYhwQDKwATgIJlkD8AXFAM7kCWAdgOYDaAuhgL4ZQkKFTIpYiLgHJ8hEuTHS+AYwD2bZlDwS2AVwA2B7Y21sQJ0dQx4AdADM1ZAKJ4VACwAUngF4BKFAA+KH8MIA diff --git a/packages/server/utils/AtlassianServerManager.ts b/packages/server/utils/AtlassianServerManager.ts index d84a448f7be..2cd82d3e8e4 100644 --- a/packages/server/utils/AtlassianServerManager.ts +++ b/packages/server/utils/AtlassianServerManager.ts @@ -358,7 +358,38 @@ class AtlassianServerManager extends AtlassianManager { async getAllProjects(cloudIds: string[]) { const projects = [] as (JiraProject & {cloudId: string})[] let error: Error | undefined - const getProjectPage = async (cloudId: string, url: string): Promise => { + const getProjectsPage = async ( + cloudId: string, + startAt: number, + maxResults: number + ): Promise => { + const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/project/search?orderBy=name&startAt=${startAt}` + const res = await this.get(url) + if (res instanceof Error || res instanceof RateLimitError) { + error = res + } else { + const pagedProjects = res.values.map((project) => ({ + ...project, + cloudId + })) + projects.push(...pagedProjects) + + if (pagedProjects.length < maxResults && res.nextPage) { + Logger.log( + 'Underfetched in getAllProjects, requested', + maxResults, + 'got', + pagedProjects.length + ) + const nextStart = res.startAt + pagedProjects.length + const nextMaxResults = maxResults - pagedProjects.length + return getProjectsPage(cloudId, nextStart, nextMaxResults) + } + } + } + + const getProjects = async (cloudId: string) => { + const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/project/search?orderBy=name` const res = await this.get(url) if (res instanceof Error || res instanceof RateLimitError) { error = res @@ -369,19 +400,20 @@ class AtlassianServerManager extends AtlassianManager { })) projects.push(...pagedProjects) if (res.nextPage) { - Logger.log('AtlassianServerManager.getAllProjects fetching more results', res.total) - return getProjectPage(cloudId, res.nextPage) + const {total} = res + const nextStart = res.startAt + pagedProjects.length + const fetches = [] as Array> + // 50 is the default maxResults for Jira, Jira does not respond with more than that + const maxResults = 50 + for (let i = nextStart; i < total; i += maxResults) { + fetches.push(getProjectsPage(cloudId, i, maxResults)) + } + await Promise.all(fetches) } } } - await Promise.all( - cloudIds.map((cloudId) => - getProjectPage( - cloudId, - `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/project/search?orderBy=name&maxResults=500` - ) - ) - ) + + await Promise.all(cloudIds.map((cloudId) => getProjects(cloudId))) if (error) { Logger.log('getAllProjects ERROR:', error) diff --git a/yarn.lock b/yarn.lock index 69cf14bcce1..17637e0e348 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3159,6 +3159,18 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb" integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg== +"@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + "@eslint/eslintrc@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" @@ -7572,6 +7584,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json-stable-stringify@^1.0.32": version "1.0.33" resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.33.tgz#099b0712d824d15e2660c20e1c16e6a8381f308c" @@ -7699,15 +7716,22 @@ undici-types "~5.25.1" "@types/node@^16.11.62": - version "16.11.62" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.62.tgz#bab2e6208531321d147eda20c38e389548cd5ffc" - integrity sha512-K/ggecSdwAAy2NUW4WKmF4Rc03GKbsfP+k326UWgckoS+Rzd2PaWbjk76dSmqdLQvLTJAO9axiTUJ6488mFsYQ== + version "16.18.85" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.85.tgz#17b5338c958efd67b064b92fbef41ad0333c397b" + integrity sha512-un7Bj6CPCRLxG2qp+9enNVFuRWCDKKOS6Q/FSpJ4xyrpLNJnRdAQERM2sJ6esaGvl02nK6kiGcMTb0pqknm62g== "@types/node@^18.11.18": version "18.17.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.18.tgz#acae19ad9011a2ab3d792232501c95085ba1838f" integrity sha512-/4QOuy3ZpV7Ya1GTRz5CYSz3DgkKpyUptXuQ5PPce7uuyJAOR7r9FhkmxJfvcNUXyklbC63a+YvB3jxy7s9ngw== +"@types/node@^20.11.17": + version "20.11.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.17.tgz#cdd642d0e62ef3a861f88ddbc2b61e32578a9292" + integrity sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw== + dependencies: + undici-types "~5.26.4" + "@types/nodemailer@^6.4.14": version "6.4.14" resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.14.tgz#5c81a5e856db7f8ede80013e6dbad7c5fb2283e2" @@ -7892,6 +7916,11 @@ "@types/glob" "*" "@types/node" "*" +"@types/semver@^7.5.0": + version "7.5.6" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339" + integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== + "@types/send@*": version "0.17.1" resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301" @@ -8044,85 +8073,91 @@ resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e" integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g== -"@typescript-eslint/eslint-plugin@5.17.0", "@typescript-eslint/eslint-plugin@^5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.17.0.tgz#704eb4e75039000531255672bf1c85ee85cf1d67" - integrity sha512-qVstvQilEd89HJk3qcbKt/zZrfBZ+9h2ynpAGlWjWiizA7m/MtLT9RoX6gjtpE500vfIg8jogAkDzdCxbsFASQ== +"@typescript-eslint/eslint-plugin@^6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" + integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== dependencies: - "@typescript-eslint/scope-manager" "5.17.0" - "@typescript-eslint/type-utils" "5.17.0" - "@typescript-eslint/utils" "5.17.0" - debug "^4.3.2" - functional-red-black-tree "^1.0.1" - ignore "^5.1.8" - regexpp "^3.2.0" - semver "^7.3.5" - tsutils "^3.21.0" + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/type-utils" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" -"@typescript-eslint/parser@5.17.0", "@typescript-eslint/parser@^5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.17.0.tgz#7def77d5bcd8458d12d52909118cf3f0a45f89d5" - integrity sha512-aRzW9Jg5Rlj2t2/crzhA2f23SIYFlF9mchGudyP0uiD6SenIxzKoLjwzHbafgHn39dNV/TV7xwQkLfFTZlJ4ig== +"@typescript-eslint/parser@^6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== dependencies: - "@typescript-eslint/scope-manager" "5.17.0" - "@typescript-eslint/types" "5.17.0" - "@typescript-eslint/typescript-estree" "5.17.0" - debug "^4.3.2" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" -"@typescript-eslint/scope-manager@5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.17.0.tgz#4cea7d0e0bc0e79eb60cad431c89120987c3f952" - integrity sha512-062iCYQF/doQ9T2WWfJohQKKN1zmmXVfAcS3xaiialiw8ZUGy05Em6QVNYJGO34/sU1a7a+90U3dUNfqUDHr3w== +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== dependencies: - "@typescript-eslint/types" "5.17.0" - "@typescript-eslint/visitor-keys" "5.17.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" -"@typescript-eslint/type-utils@5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.17.0.tgz#1c4549d68c89877662224aabb29fbbebf5fc9672" - integrity sha512-3hU0RynUIlEuqMJA7dragb0/75gZmwNwFf/QJokWzPehTZousP/MNifVSgjxNcDCkM5HI2K22TjQWUmmHUINSg== +"@typescript-eslint/type-utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" + integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== dependencies: - "@typescript-eslint/utils" "5.17.0" - debug "^4.3.2" - tsutils "^3.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + debug "^4.3.4" + ts-api-utils "^1.0.1" -"@typescript-eslint/types@5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.17.0.tgz#861ec9e669ffa2aa9b873dd4d28d9b1ce26d216f" - integrity sha512-AgQ4rWzmCxOZLioFEjlzOI3Ch8giDWx8aUDxyNw9iOeCvD3GEYAB7dxWGQy4T/rPVe8iPmu73jPHuaSqcjKvxw== +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== -"@typescript-eslint/typescript-estree@5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.17.0.tgz#a7cba7dfc8f9cc2ac78c18584e684507df4f2488" - integrity sha512-X1gtjEcmM7Je+qJRhq7ZAAaNXYhTgqMkR10euC4Si6PIjb+kwEQHSxGazXUQXFyqfEXdkGf6JijUu5R0uceQzg== +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== dependencies: - "@typescript-eslint/types" "5.17.0" - "@typescript-eslint/visitor-keys" "5.17.0" - debug "^4.3.2" - globby "^11.0.4" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + globby "^11.1.0" is-glob "^4.0.3" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/utils@5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.17.0.tgz#549a9e1d491c6ccd3624bc3c1b098f5cfb45f306" - integrity sha512-DVvndq1QoxQH+hFv+MUQHrrWZ7gQ5KcJzyjhzcqB1Y2Xes1UQQkTRPUfRpqhS8mhTWsSb2+iyvDW1Lef5DD7vA== - dependencies: - "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.17.0" - "@typescript-eslint/types" "5.17.0" - "@typescript-eslint/typescript-estree" "5.17.0" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" + integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + semver "^7.5.4" -"@typescript-eslint/visitor-keys@5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.17.0.tgz#52daae45c61b0211b4c81b53a71841911e479128" - integrity sha512-6K/zlc4OfCagUu7Am/BD5k8PSWQOgh34Nrv9Rxe2tBzlJ7uOeJ/h7ugCGDCeEZHT6k2CJBhbk9IsbkPI0uvUkA== +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== dependencies: - "@typescript-eslint/types" "5.17.0" - eslint-visitor-keys "^3.0.0" + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" "@webassemblyjs/ast@1.11.5", "@webassemblyjs/ast@^1.11.5": version "1.11.5" @@ -8542,13 +8577,13 @@ amp@0.3.1, amp@~0.3.1: resolved "https://registry.yarnpkg.com/amp/-/amp-0.3.1.tgz#6adf8d58a74f361e82c1fa8d389c079e139fc47d" integrity sha1-at+NWKdPNh6CwfqNOJwHnhOfxH0= -analytics-node@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/analytics-node/-/analytics-node-5.2.0.tgz#ef167bbf0d51f630e96d3abe604ce449b50a2584" - integrity sha512-JAc0K7J//QKGGX2mfwBE7wyG/Rh4W0BATB6CncwYhVX1qLjiXwHmB7bYr9PgJIer5M6jKMOhZoO5lxiruQk9BQ== +analytics-node@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/analytics-node/-/analytics-node-6.2.0.tgz#8ae2ebc73d85e5b2aac8d366b974ad36996f629d" + integrity sha512-NLU4tCHlWt0tzEaFQL7NIoWhq2KmQSmz0JvyS2lYn6fc4fEjTMSabhJUx8H1r5995FX8fE3rZ15uIHU6u+ovlQ== dependencies: "@segment/loosely-validate-event" "^2.0.0" - axios "^0.21.4" + axios "^0.27.2" axios-retry "3.2.0" lodash.isstring "^4.0.1" md5 "^2.2.1" @@ -8904,13 +8939,21 @@ axios-retry@3.2.0: dependencies: is-retry-allowed "^1.1.0" -axios@0.21.4, axios@^0.21.0, axios@^0.21.4: +axios@0.21.4, axios@^0.21.0: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== dependencies: follow-redirects "^1.14.0" +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + axios@^1.0.0: version "1.3.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.2.tgz#7ac517f0fa3ec46e0e636223fd973713a09c72b3" @@ -11130,7 +11173,6 @@ draft-js-utils@^1.4.0: "draft-js@https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6": version "0.10.5" - uid "025fddba56f21aaf3383aee778e0b17025c9a7bc" resolved "https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6#025fddba56f21aaf3383aee778e0b17025c9a7bc" dependencies: fbjs "^0.8.15" @@ -11374,13 +11416,14 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: - version "0.10.62" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" - integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@^0.10.62, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== dependencies: es6-iterator "^2.0.3" es6-symbol "^3.1.3" + esniff "^2.0.1" next-tick "^1.1.0" es6-iterator@^2.0.3: @@ -11498,7 +11541,7 @@ eslint-plugin-react@^7.16.0: semver "^6.3.0" string.prototype.matchall "^4.0.6" -eslint-scope@5.1.1, eslint-scope@^5.1.1: +eslint-scope@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== @@ -11526,7 +11569,7 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.2.0, eslint-visitor-keys@^3.4.1: +eslint-visitor-keys@^3.2.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== @@ -11572,6 +11615,16 @@ eslint@^8.2.0, eslint@^8.8.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + espree@^9.0.0, espree@^9.2.0, espree@^9.3.0: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" @@ -11836,6 +11889,17 @@ fast-glob@^3.1.1, fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.4: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-patch@^3.0.0-1: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947" @@ -12098,6 +12162,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.14.9: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + foreground-child@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" @@ -12619,7 +12688,7 @@ globals@^13.6.0, globals@^13.9.0: dependencies: type-fest "^0.20.2" -globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.0.4: +globby@^11.0.1, globby@^11.0.2, globby@^11.0.3: version "11.0.4" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== @@ -12631,6 +12700,18 @@ globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.0.4: merge2 "^1.3.0" slash "^3.0.0" +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + globby@^13.1.1: version "13.1.3" resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.3.tgz#f62baf5720bcb2c1330c8d4ef222ee12318563ff" @@ -12726,6 +12807,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + graphiql@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/graphiql/-/graphiql-3.0.0.tgz#9ea10cb552759ae69a14c72bf219e9f425a607d7" @@ -13269,11 +13355,16 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.0.4, ignore@^5.1.4, ignore@^5.1.8, ignore@^5.2.0: +ignore@^5.0.4, ignore@^5.1.4, ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +ignore@^5.2.4: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + image-size@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.0.tgz#58b31fe4743b1cec0a0ac26f5c914d3c5b2f0750" @@ -15637,6 +15728,13 @@ minimatch@3.0.5: dependencies: brace-expansion "^1.1.7" +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -20457,6 +20555,11 @@ ts-algebra@^1.2.0: resolved "https://registry.yarnpkg.com/ts-algebra/-/ts-algebra-1.2.0.tgz#f91c481207a770f0d14d055c376cbee040afdfc9" integrity sha512-kMuJJd8B2N/swCvIvn1hIFcIOrLGbWl9m/J6O3kHx9VRaevh00nvgjPiEGaRee7DRaAczMYR2uwWvXU22VFltw== +ts-api-utils@^1.0.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b" + integrity sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA== + ts-app-env@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/ts-app-env/-/ts-app-env-1.4.2.tgz#2a76d19d61b66c6bc92ff90e3a0e6db6c545a776" @@ -20574,7 +20677,7 @@ tslib@1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== -tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.10.0, tslib@^1.11.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -20599,13 +20702,6 @@ tslib@~2.5.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -20729,10 +20825,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@4.9.5, "typescript@^3 || ^4", typescript@^4.2.4, typescript@^4.5.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@4.9.5, "typescript@^3 || ^4", typescript@^4.2.4, typescript@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== uWebSockets.js@uNetworking/uWebSockets.js#v20.34.0: version "20.34.0" @@ -20778,6 +20874,11 @@ undici-types@~5.25.1: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + undici@^5.26.2: version "5.26.3" resolved "https://registry.yarnpkg.com/undici/-/undici-5.26.3.tgz#ab3527b3d5bb25b12f898dfd22165d472dd71b79" @@ -20785,6 +20886,13 @@ undici@^5.26.2: dependencies: "@fastify/busboy" "^2.0.0" +undici@^5.28.3: + version "5.28.3" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.3.tgz#a731e0eff2c3fcfd41c1169a869062be222d1e5b" + integrity sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA== + dependencies: + "@fastify/busboy" "^2.0.0" + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" From 94be730e13dc8597f7a14908e1fee42a7fdb1ec5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 21:30:22 +0000 Subject: [PATCH 04/19] chore(release): Test v7.19.7 (#9484) * chore(env vars): Stripe vars moved to the Integrations section (#9427) * chore: fix misleading `isLead` field name on `Team` (#9413) * chore: fix misleading `isLead` field name on `Team` The field indicates whether the viewer is the lead, but when used in a query for a different user, the result could be read wrong. * Fix Team.isLead dependencies * feat: remove team template limit (#9424) * update error message and increase template limit * remove max team template limits * remove canClone prop from CloneTemplate * remove unused threshold * remove unused threshold * feat: Add Google calendar meeting series for recurrence (#9380) * feat: Add recurrence to GCal events * Fun with timezones * fix: Increase the number of projects fetched per request from Atlassian (#9435) We ran into timeouts in `getAllProjects`, presumably because we're doing too many roundtrips. As a quick fix, increse the number of projects fetched per request from 50 to 500. * chore(deps): bump ip from 1.1.8 to 1.1.9 (#9442) Bumps [ip](https://github.com/indutny/node-ip) from 1.1.8 to 1.1.9. - [Commits](https://github.com/indutny/node-ip/compare/v1.1.8...v1.1.9) --- updated-dependencies: - dependency-name: ip dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(release): release v7.17.0 (#9428) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * feat(standalone-deployment): Standalone host deployment improved and documented (#9445) * Docker compose stack improved * Remove unused containers from docker-compse and add useful comment on .env.example about PGSSLMODE * Docker compose profiles added. Documentation extended on how to use the profiles to manage the stack. * README fixed as docker compose up and down commands were not working * Typo fixed and docker-compose command replaced by docker compose * feat: support env-defined saml issuer for PPMIs (#9455) * feat: support env-defined saml issuer for PPMIs Signed-off-by: Matt Krick * feat: support single SAML for entire tenant Signed-off-by: Matt Krick --------- Signed-off-by: Matt Krick * chore: Associate logs with traces (#9444) * chore: Associate logs with traces Add trace information to log output for server side log statements. This does not include logging from code exclusively used for debugging, deploying or development. * Actually add the logger * Fix DD_LOGS_INJECTION check * chore(release): release v7.18.0 (#9450) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * chore: no force-push to prod (#9401) Signed-off-by: Matt Krick * chore(release): release v7.18.1 (#9459) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * feat: embedder service (#9417) * feat: add embedder service --------- Signed-off-by: Matt Krick Co-authored-by: Matt Krick * merge production to avoid force push (#9461) Signed-off-by: Matt Krick * chore(release): release v7.19.0 (#9460) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: checkout prod before merging it (#9463) Signed-off-by: Matt Krick * chore(release): release v7.19.1 (#9464) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: mrege origin/production strategy (#9465) Signed-off-by: Matt Krick * chore(release): release v7.19.2 (#9466) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: force push 5 (#9467) Signed-off-by: Matt Krick * chore(release): release v7.19.3 (#9468) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: limit invites from spammers (#9416) * fix: limit invites from spammers * update where we check pending emails * check total plus pending invites * use invitees instead of pending * fix: Fetch Jira projects in parallel (#9456) Previously we tried to fetch more projects per page, but Jira only ever returns 50 max. Instead once we know how many projects there are after fetching the first page, we fetch all remaining pages in parallel. * fix: replace lone surrogates in draft-js content (#9415) * fix: replace lone surrogates in draft-js content Signed-off-by: Matt Krick * fix typo Signed-off-by: Matt Krick * fix: eslint errors Signed-off-by: Matt Krick --------- Signed-off-by: Matt Krick * chore(deps): bump es5-ext from 0.10.62 to 0.10.64 (#9457) Bumps [es5-ext](https://github.com/medikoo/es5-ext) from 0.10.62 to 0.10.64. - [Release notes](https://github.com/medikoo/es5-ext/releases) - [Changelog](https://github.com/medikoo/es5-ext/blob/main/CHANGELOG.md) - [Commits](https://github.com/medikoo/es5-ext/compare/v0.10.62...v0.10.64) --- updated-dependencies: - dependency-name: es5-ext dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: packages/server/package.json to reduce vulnerabilities (#9434) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-UNDICI-6252336 Co-authored-by: snyk-bot * fix: packages/server/package.json to reduce vulnerabilities (#9392) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-NODEMAILER-6219989 Co-authored-by: snyk-bot * fix: packages/server/package.json to reduce vulnerabilities (#9298) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-FOLLOWREDIRECTS-6141137 Co-authored-by: snyk-bot * chore(deps): bump follow-redirects from 1.14.8 to 1.15.4 (#9312) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.8 to 1.15.4. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.8...v1.15.4) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: Matt Krick Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matt Krick * chore: add upload to GCS step in ironbank (#9471) * add upload to GCS step in ironbank * update workflow name * chore(release): release v7.19.4 (#9470) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: Fix seasonal templates for leap years (#9476) * fix: Fix seasonal templates for leap years It would produce invalid dates on February 29th. * Master was not clean * chore(release): release v7.19.5 (#9477) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: After parameter for meetingCount was ignored (#9479) * chore(docker-build): simplify the docker build process and reduce docker image size (#9447) * Dockerfile basic created. Improvements added to reduce build time and size (down from 795MB to 445MB, depending on systemtap). Readme reduced, removing the old process used to build the image. * basic-env file using a RethinkDB database name that is clearly dedicated to the building proces. * Readme improved to run all three components * Unused dockerfiles removed. Docker entrypoint renamed. Docker Readme adapted * Legacy build kept in both dockerfile and env file. Readme adapted to use the new basic image. Build GH workflow adapted to use the new basic.dockerfile. * chore(release): release v7.19.6 (#9480) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix(docker-build): home folder is /home/node now (#9482) * chore(release): release v7.19.7 (#9483) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Signed-off-by: Matt Krick Co-authored-by: Rafa <101704572+rafaelromcar-parabol@users.noreply.github.com> Co-authored-by: Georg Bremer Co-authored-by: Nick O'Ferrall Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> Co-authored-by: Matt Krick Co-authored-by: Jordan Husney Co-authored-by: adaniels-parabol <71724289+adaniels-parabol@users.noreply.github.com> Co-authored-by: snyk-bot Co-authored-by: Dale Bumblis <135627447+dbumblis-parabol@users.noreply.github.com> Co-authored-by: github-actions --- .github/workflows/build.yml | 3 +- .release-please-manifest.json | 2 +- CHANGELOG.md | 19 +++ docker/parabol-ubi/docker-build/README.md | 82 ++++-------- .../docker-build/dockerfiles/basic.dockerfile | 29 +++++ .../dockerfiles/parabol.dockerfile | 5 +- .../dockerfiles/pipeline.dockerfile | 117 ------------------ .../dockerfiles/security-test.dockerfile | 60 --------- .../{buildenv => docker-entrypoint.sh} | 0 .../docker-build/environments/basic-env | 17 +++ .../environments/{buildenv => legacy-build} | 0 .../docker-build/environments/local-buildenv | 54 -------- package.json | 2 +- packages/chronos/package.json | 4 +- packages/client/package.json | 2 +- packages/gql-executor/package.json | 6 +- packages/integration-tests/package.json | 2 +- .../server/graphql/public/types/Company.ts | 2 +- packages/server/package.json | 4 +- 19 files changed, 103 insertions(+), 307 deletions(-) create mode 100644 docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile delete mode 100644 docker/parabol-ubi/docker-build/dockerfiles/pipeline.dockerfile delete mode 100644 docker/parabol-ubi/docker-build/dockerfiles/security-test.dockerfile rename docker/parabol-ubi/docker-build/entrypoints/{buildenv => docker-entrypoint.sh} (100%) create mode 100644 docker/parabol-ubi/docker-build/environments/basic-env rename docker/parabol-ubi/docker-build/environments/{buildenv => legacy-build} (100%) delete mode 100644 docker/parabol-ubi/docker-build/environments/local-buildenv diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b7cf97ce90..9e2e74b3df6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true env: - PARABOL_DOCKERFILE: ./docker/parabol-ubi/docker-build/dockerfiles/pipeline.dockerfile + PARABOL_DOCKERFILE: ./docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile PARABOL_BUILD_ENV_PATH: docker/parabol-ubi/docker-build/environments/pipeline jobs: build: @@ -114,7 +114,6 @@ jobs: context: . build-args: | "_NODE_VERSION=${{ env.NODE_VERSION }}" - "_SECURITY_ENABLED=true" push: true tags: | "${{ secrets.GCP_AR_PARABOL_DEV }}:${{github.sha}}" diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6e9dcf42d00..57085bcbc6b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.19.5" + ".": "7.19.7" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 080f2ea136c..7191952d8a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.19.7](https://github.com/ParabolInc/parabol/compare/v7.19.6...v7.19.7) (2024-02-29) + + +### Fixed + +* **docker-build:** home folder is /home/node now ([#9482](https://github.com/ParabolInc/parabol/issues/9482)) ([2ff4a6e](https://github.com/ParabolInc/parabol/commit/2ff4a6e6328bf437a31e9ac7984af4a55aae3d11)) + +## [7.19.6](https://github.com/ParabolInc/parabol/compare/v7.19.5...v7.19.6) (2024-02-29) + + +### Fixed + +* After parameter for meetingCount was ignored ([#9479](https://github.com/ParabolInc/parabol/issues/9479)) ([052acd1](https://github.com/ParabolInc/parabol/commit/052acd14035fe7c96af8d17ca4763be91d863a80)) + + +### Changed + +* **docker-build:** simplify the docker build process and reduce docker image size ([#9447](https://github.com/ParabolInc/parabol/issues/9447)) ([5e356c2](https://github.com/ParabolInc/parabol/commit/5e356c2566db8e32e45a1393e1b1ea27c4be0a5c)) + ## [7.19.5](https://github.com/ParabolInc/parabol/compare/v7.19.4...v7.19.5) (2024-02-29) diff --git a/docker/parabol-ubi/docker-build/README.md b/docker/parabol-ubi/docker-build/README.md index 57233e4dbcb..85c1a2d42b3 100644 --- a/docker/parabol-ubi/docker-build/README.md +++ b/docker/parabol-ubi/docker-build/README.md @@ -1,17 +1,8 @@ # docker-image-parabol -This repo was created to build a **secure** Parabol base image that is **agnostic to configuration and can be used anywhere**. Once an image is built, it can be pushed to any repository. +This repo was created to build a Parabol base image that is **agnostic to configuration and can be used anywhere**. Once an image is built, it can be pushed to any repository. -There are two possible ways to build the application: - -- **Standard build:** duild using local files, using the same Dockerfile and process used in our Docker Build pipeline. -- **Build from git:** build using a simplified process that downloads the source code and builds from scratch. - -The processes are different and the details of it can be checked in both dockerfiles. - -## Standard build - -### Requirements +## Requirements Required: @@ -21,37 +12,35 @@ Required: Recommended: -- jq installed. +- [jq](https://jqlang.github.io/jq/) installed. It is used to get the version of the application. -### Variables +## Variables | Name | Description | Possible values | Recommended value | | -------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------- | | `postgresql_tag` | PostgreSQL version from the [Docker image](https://hub.docker.com/_/postgres) | `Any tag` | `15.4` | | `rethinkdb_tag` | RethinkDB version from the [Docker image](https://hub.docker.com/_/rethinkdb) | `Any tag` | `2.4.2` | | `redis_tag` | Redis version from the [Docker image](https://hub.docker.com/_/redis) | `Any tag` | `7.0-alpine` | -| `_BUILD_ENV_PATH` | File `.env` used by the application during the build process | `Relative path from the root level of the repository` | `docker/parabol-ubi/docker-build/environments/pipeline` | +| `_BUILD_ENV_PATH` | File `.env` used by the application during the build process | `Relative path from the root level of the repository` | `docker/parabol-ubi/docker-build/environments/basic-env` | | `_NODE_VERSION` | Node version, used by Docker to use the Docker image node:\_NODE_VERSION as base image to build | `Same as in root package.json` | | -| `_DOCKERFILE` | Dockerfile used to build the image | `Relative path from the root level of the repository` | `./docker/parabol-ubi/docker-build/dockerfiles/pipeline.dockerfile` | -| `_SECURITY_ENABLED` | Enable or disable security configurations. It will add some MBs to the final image, but it will produce a secured image | `true/false` | `true` | +| `_DOCKERFILE` | Dockerfile used to build the image | `Relative path from the root level of the repository` | `./docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile` | | `_DOCKER_REPOSITORY` | The destination repository | `String` | `parabol` | | `_DOCKER_TAG` | Tag for the produced image | `String` | | Example of variables: ```commandLine -export postgresql_tag=15.4-alpine; \ +export postgresql_tag=15.4; \ export rethinkdb_tag=2.4.2; \ export redis_tag=7.0-alpine; \ -export _BUILD_ENV_PATH=docker/parabol-ubi/docker-build/environments/pipeline; \ +export _BUILD_ENV_PATH=docker/parabol-ubi/docker-build/environments/basic-env; \ export _NODE_VERSION=$(jq -r -j '.engines.node|ltrimstr("^")' package.json); \ -export _DOCKERFILE=./docker/parabol-ubi/docker-build/dockerfiles/pipeline.dockerfile; \ -export _SECURITY_ENABLED=true; \ +export _DOCKERFILE=./docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile; \ export _DOCKER_REPOSITORY=parabol; \ export _DOCKER_TAG=test-image ``` -### Building the image +## Building the image The application must be already built locally using the command `yarn build --no-deps` mode. @@ -90,7 +79,7 @@ yarn build --no-deps - **Build the docker image:** ```commandLine -docker build -t $_DOCKER_REPOSITORY:$_DOCKER_TAG -f $_DOCKERFILE --build-arg _NODE_VERSION=$_NODE_VERSION --build-arg _SECURITY_ENABLED=$_SECURITY_ENABLED . +docker build -t $_DOCKER_REPOSITORY:$_DOCKER_TAG -f $_DOCKERFILE --build-arg _NODE_VERSION=$_NODE_VERSION . ``` > Some build tips @@ -119,57 +108,30 @@ It will produce a Docker image tagged as `${_DOCKER_REPOSITORY}:${_DOCKER_TAG}`. docker images $_DOCKER_REPOSITORY:$_DOCKER_TAG ``` -## Build from git - -This version of the Dockerfile downloads the application during the docker build process and differs in other +## Run the application using a docker image -Modify the version export below e.g. update vX.X.X and run the export command and the docker command. The command below will create a temp postgres container (this allows pgtype files to be generated) and then build the docker image with a temp .env file. +_Assumes redis, rethinkdb, and postgres already running to have operational stack._ -- Change `environments/buildenv` connection string names form container names to localhost for local image build. -- Use `_PARABOL_GIT_REF` to select the reference in Parabol's Git repository. It can be any tag or branch, but it is recommended to use released tags as `v6.69.0`. By default it buils a local image using only `parabol` as repository. -- Use `_DOCKER_REPOSITORY` to build the image for a remote repository (ex: `gcr.io/parabol-proving-ground/parabol`) -- Use `_DOCKER_TAG` to define the tag for the new image. +The commands below will start a Parabol container on the target tag specified in \_DOCKER_TAG export. It will volume mount a .env in your current working directory to the container, so you can pass in any .env in your current working directory. -```commandLine -export postgresql_tag=15.4-alpine; \ -export rethinkdb_tag=2.4.2; \ -export redis_tag=7.0-alpine; \ -export _BUILD_ENV_PATH=environments/local-buildenv \ -export _NODE_VERSION=20.11.0 \ -export _DOCKER_REPOSITORY=parabol \ -export _PARABOL_GIT_REF=vX.X.X \ -export _DOCKER_TAG=vX.X.X -``` +For a more detailed how-to deploy Parabol, please go to the section [docker-host-st](https://github.com/ParabolInc/parabol/tree/master/docker/parabol-ubi/docker-host-st) -Now you can build the image +- Run the PreDeploy script ```commandLine -docker run --name temp-postgres --network=host -e POSTGRES_PASSWORD=temppassword -e POSTGRES_USER=tempuser -e POSTGRES_DB=tempdb -d -p 5432:5432 postgres:${postgresql_tag} && \ -docker run --name temp-rethinkdb --network=host -d -p 28015:28015 -p 29015:29015 -p 8080:8080 rethinkdb:${rethinkdb_tag} && \ -docker run --name temp-redis --network=host -d -p 6379:6379 redis:${redis_tag} && \ -docker build --no-cache --network=host -t ${_DOCKER_REPOSITORY}:${_DOCKER_TAG} -f ./dockerfiles/parabol.dockerfile --build-arg _PARABOL_GIT_REF=${_PARABOL_GIT_REF} --build-arg _NODE_VERSION=$_NODE_VERSION --build-arg _BUILD_ENV_PATH=${_BUILD_ENV_PATH} . && \ -docker stop temp-postgres temp-rethinkdb temp-redis && docker rm temp-postgres temp-rethinkdb temp-redis -f || docker stop temp-postgres temp-rethinkdb temp-redis && docker rm temp-postgres temp-rethinkdb temp-redis -f -``` - -If `_DOCKER_REPOSITORY` wasn't local and you want to push the image, you can run then: +export _DOCKER_REPOSITORY=parabol; \ +export _DOCKER_TAG=vX.X.X -```commandLine -docker push ${_DOCKER_REPOSITORY}:${_DOCKER_TAG} +docker run --name=parabol-predeploy --network=host -v $(pwd)/.env:/home/node/parabol/.env ${_DOCKER_REPOSITORY}:${_DOCKER_TAG} /bin/bash -c "node dist/preDeploy.js" ``` -## Run the application using a docker image - -_Assumes redis, rethinkdb, and postgres already running to have operational stack._ - -The commands below will start a Parabol container on the target tag specified in \_DOCKER_TAG export. It will volume mount a .env in your current working directory to the container, so you can pass in any .env in your current working directory. - - Start GraphQL ```commandLine export _DOCKER_REPOSITORY=parabol; \ export _DOCKER_TAG=vX.X.X -docker run --name=parabolgraphql --network=host -v $(pwd)/.env:/home/node/parabol/.env ${_DOCKER_REPOSITORY}:${_DOCKER_TAG} /bin/bash -c "yarn predeploy && NODE_ENV=production && node ./dist/gqlExecutor.js" || docker container rm parabolgraphql -f +docker run --name=parabol-gql-executor --network=host -v $(pwd)/.env:/home/node/parabol/.env ${_DOCKER_REPOSITORY}:${_DOCKER_TAG} /bin/bash -c "node ./dist/gqlExecutor.js" || docker container rm parabol-gql-executor -f ``` - Start Web Server @@ -178,7 +140,7 @@ docker run --name=parabolgraphql --network=host -v $(pwd)/.env:/home/node/parabo export _DOCKER_REPOSITORY=parabol; \ export _DOCKER_TAG=vX.X.X -docker run --name=parabol --network=host -v $(pwd)/.env:/home/node/parabol/.env -p 3000:3000 ${_DOCKER_REPOSITORY}:${_DOCKER_TAG} /bin/bash -c "yarn predeploy && NODE_ENV=production && node ./dist/web.js" || docker container rm parabol -f +docker run --name=parabol-web-server --network=host -v $(pwd)/.env:/home/node/parabol/.env -p 3000:3000 ${_DOCKER_REPOSITORY}:${_DOCKER_TAG} /bin/bash -c "node ./dist/web.js" || docker container rm parabol-web-server -f ``` -To stop the container, just open another terminal and enter `docker container stop parabol` +To stop the container, just open another terminal and enter `docker container stop parabol-COMPONENT` diff --git a/docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile b/docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile new file mode 100644 index 00000000000..077d95a8fb6 --- /dev/null +++ b/docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile @@ -0,0 +1,29 @@ +ARG _NODE_VERSION=${_NODE_VERSION} +FROM node:${_NODE_VERSION}-bookworm-slim as base + +ENV HOME=/home/node \ + USER=node + +ENV NPM_CONFIG_PREFIX=/home/node/.npm-global +ENV PORT=3000 + +COPY --chown=node --chmod=755 docker/parabol-ubi/docker-build/entrypoints/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +COPY --chown=node docker/parabol-ubi/docker-build/tools/ip-to-server_id ${HOME}/tools/ip-to-server_id + +# Required for pushToCDN to work with FILE_STORE_PROVIDER set to 'local' +RUN mkdir -p ${HOME}/parabol/self-hosted && \ + chown node:node ${HOME}/parabol/self-hosted + +COPY --chown=node .env.example ${HOME}/parabol/.env.example + +# The application requires a yarn.lock file on the root folder to identify it +COPY --chown=node yarn.lock ${HOME}/parabol/yarn.lock +COPY --chown=node build ${HOME}/parabol/build +COPY --chown=node dist ${HOME}/parabol/dist + +WORKDIR ${HOME}/parabol/ + +USER node +EXPOSE ${PORT} + +ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/docker/parabol-ubi/docker-build/dockerfiles/parabol.dockerfile b/docker/parabol-ubi/docker-build/dockerfiles/parabol.dockerfile index f2f7466af8b..2688d19ee8d 100644 --- a/docker/parabol-ubi/docker-build/dockerfiles/parabol.dockerfile +++ b/docker/parabol-ubi/docker-build/dockerfiles/parabol.dockerfile @@ -1,9 +1,10 @@ +# DO NOT DELETE. Legacy docker file for versions still in use. Delete only when all Parabol instances are using the newest docker image. ARG _NODE_VERSION=${_NODE_VERSION} #base build for dev deps FROM node:${_NODE_VERSION} as base ARG _PARABOL_GIT_REF=${_PARABOL_GIT_REF} -ARG _BUILD_ENV_PATH=environments/buildenv +ARG _BUILD_ENV_PATH=environments/legacy-build ENV NPM_CONFIG_PREFIX=/home/node/.npm-global WORKDIR /home/node @@ -45,7 +46,7 @@ COPY --from=base /usr/local/lib/node_modules /usr/local/lib/node_modules COPY --from=base /opt /opt COPY --from=base /home/node/parabol/ ${HOME}/parabol RUN rm -rf ${HOME}/parabol/.env -COPY entrypoints/buildenv /usr/local/bin/docker-entrypoint.sh +COPY entrypoints/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh COPY security /security COPY ./tools/ip-to-server_id /home/node/tools/ip-to-server_id diff --git a/docker/parabol-ubi/docker-build/dockerfiles/pipeline.dockerfile b/docker/parabol-ubi/docker-build/dockerfiles/pipeline.dockerfile deleted file mode 100644 index 5194efcbe4d..00000000000 --- a/docker/parabol-ubi/docker-build/dockerfiles/pipeline.dockerfile +++ /dev/null @@ -1,117 +0,0 @@ -ARG _NODE_VERSION=${_NODE_VERSION} -FROM node:${_NODE_VERSION} as base - -ENV NPM_CONFIG_PREFIX=/home/node/.npm-global -ENV PORT=3000 - -RUN apt update -y && \ - apt install systemtap -y - -USER node -EXPOSE ${PORT} - -ENTRYPOINT ["docker-entrypoint.sh"] - -# Final image - copies in parabol build and applies all security configurations to container if enabled -FROM redhat/ubi9:9.2 - -ARG _SECURITY_ENABLED="true" - -ENV HOME=/home/node \ - USER=node - -ENV PORT=3000 - -RUN groupadd -g 1000 node && \ - useradd -r -u 1000 -m -s /sbin/nologin -g node node - -COPY --from=base /usr/local/bin /usr/local/bin -COPY --from=base /usr/local/include /usr/local/include -COPY --from=base /usr/local/share/man /usr/local/share/man -COPY --from=base /usr/local/share/doc /usr/local/share/doc -COPY --from=base /usr/share/systemtap /usr/local/share/systemtap -COPY --from=base /usr/local/lib/node_modules /usr/local/lib/node_modules -COPY --from=base /opt /opt - -# Security -COPY docker/parabol-ubi/docker-build/security /security - -RUN if [ "$_SECURITY_ENABLED" = "true" ]; then \ - echo Update packages and install security patches && \ - sed -i "s/enabled=1/enabled=0/" /etc/dnf/plugins/subscription-manager.conf && \ - echo "exclude=filesystem-*" >> /etc/dnf/dnf.conf && \ - chmod +x /security/*.sh && \ - dnf repolist && \ - dnf update -y && \ - echo "* hard maxlogins 10" > /etc/security/limits.d/maxlogins.conf && \ - /security/xccdf_org.ssgproject.content_rule_account_disable_post_pw_expiration.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_logon_fail_delay.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_max_concurrent_login_sessions.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_maximum_age_login_defs.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_minimum_age_login_defs.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_minlen_login_defs.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dcredit.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dictcheck.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_difok.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_lcredit.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxclassrepeat.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxrepeat.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minclass.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minlen.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ocredit.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_password_auth.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_system_auth.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ucredit.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_unix_remember.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny_root.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_interval.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_unlock_time.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_bashrc.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_csh_cshrc.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_login_defs.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_profile.sh && \ - /security/xccdf_org.ssgproject.content_rule_banner_etc_issue.sh && \ - /security/xccdf_org.ssgproject.content_rule_configure_kerberos_crypto_policy.sh && \ - /security/xccdf_org.ssgproject.content_rule_configure_openssl_crypto_policy.sh && \ - /security/xccdf_org.ssgproject.content_rule_coredump_disable_backtraces.sh && \ - /security/xccdf_org.ssgproject.content_rule_coredump_disable_storage.sh && \ - /security/xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_burstaction.sh && \ - /security/xccdf_org.ssgproject.content_rule_disable_users_coredumps.sh && \ - /security/xccdf_org.ssgproject.content_rule_display_login_attempts.sh && \ - /security/xccdf_org.ssgproject.content_rule_ensure_gpgcheck_local_packages.sh && \ - /security/xccdf_org.ssgproject.content_rule_file_groupownership_system_commands_dirs.sh && \ - /security/xccdf_org.ssgproject.content_rule_no_empty_passwords.sh && \ - /security/xccdf_org.ssgproject.content_rule_openssl_use_strong_entropy.sh && \ - /security/xccdf_org.ssgproject.content_rule_package_crypto-policies_installed.sh && \ - /security/xccdf_org.ssgproject.content_rule_package_iptables_installed.sh && \ - dnf clean all && \ - rm -rf /var/cache/dnf/ /var/tmp/* /tmp/* /var/tmp/.???* /tmp/.???* && \ - chmod g-s /opt/yarn-v*/bin /opt/yarn-v*/lib && \ - chgrp -R root /opt/yarn-v* && \ - chgrp root /opt/yarn-v*/lib/* /opt/yarn-v*/bin/* /opt/yarn-v*/*; \ - else \ - echo "Security checks disabled."; \ - fi - -RUN rm -rf /security/ - -COPY --chown=node --chmod=755 docker/parabol-ubi/docker-build/entrypoints/buildenv /usr/local/bin/docker-entrypoint.sh -COPY --chown=node docker/parabol-ubi/docker-build/tools/ip-to-server_id ${HOME}/tools/ip-to-server_id - -# The application requires a yarn.lock file on the root folder to identify it -COPY --chown=node yarn.lock ${HOME}/parabol/yarn.lock -# Required for pushToCDN to work with FILE_STORE_PROVIDER set to 'local' -RUN mkdir -p ${HOME}/parabol/self-hosted && \ - chown node:node ${HOME}/parabol/self-hosted - -COPY --chown=node .env.example ${HOME}/parabol/.env.example -COPY --chown=node build ${HOME}/parabol/build -COPY --chown=node dist ${HOME}/parabol/dist - -WORKDIR ${HOME}/parabol/ -USER node -EXPOSE ${PORT} - -ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/docker/parabol-ubi/docker-build/dockerfiles/security-test.dockerfile b/docker/parabol-ubi/docker-build/dockerfiles/security-test.dockerfile deleted file mode 100644 index 246eba76add..00000000000 --- a/docker/parabol-ubi/docker-build/dockerfiles/security-test.dockerfile +++ /dev/null @@ -1,60 +0,0 @@ -#final image -FROM redhat/ubi9:9.2 - -COPY entrypoints/buildenv /usr/local/bin/docker-entrypoint.sh -COPY security /security - -RUN echo Update packages and install security patches && \ - sed -i "s/enabled=1/enabled=0/" /etc/dnf/plugins/subscription-manager.conf && \ - echo "exclude=filesystem-*" >> /etc/dnf/dnf.conf && \ - chmod +x /security/*.sh && \ - dnf repolist && \ - dnf update -y && \ - echo "* hard maxlogins 10" > /etc/security/limits.d/maxlogins.conf && \ - /security/xccdf_org.ssgproject.content_rule_account_disable_post_pw_expiration.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_logon_fail_delay.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_max_concurrent_login_sessions.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_maximum_age_login_defs.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_minimum_age_login_defs.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_minlen_login_defs.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dcredit.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dictcheck.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_difok.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_lcredit.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxclassrepeat.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxrepeat.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minclass.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minlen.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ocredit.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_password_auth.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_system_auth.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ucredit.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_password_pam_unix_remember.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny_root.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_interval.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_unlock_time.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_bashrc.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_csh_cshrc.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_login_defs.sh && \ - /security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_profile.sh && \ - /security/xccdf_org.ssgproject.content_rule_banner_etc_issue.sh && \ - /security/xccdf_org.ssgproject.content_rule_configure_kerberos_crypto_policy.sh && \ - /security/xccdf_org.ssgproject.content_rule_configure_openssl_crypto_policy.sh && \ - /security/xccdf_org.ssgproject.content_rule_coredump_disable_backtraces.sh && \ - /security/xccdf_org.ssgproject.content_rule_coredump_disable_storage.sh && \ - /security/xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_burstaction.sh && \ - /security/xccdf_org.ssgproject.content_rule_disable_users_coredumps.sh && \ - /security/xccdf_org.ssgproject.content_rule_display_login_attempts.sh && \ - /security/xccdf_org.ssgproject.content_rule_ensure_gpgcheck_local_packages.sh && \ - /security/xccdf_org.ssgproject.content_rule_file_groupownership_system_commands_dirs.sh && \ - /security/xccdf_org.ssgproject.content_rule_no_empty_passwords.sh && \ - /security/xccdf_org.ssgproject.content_rule_openssl_use_strong_entropy.sh && \ - /security/xccdf_org.ssgproject.content_rule_package_crypto-policies_installed.sh && \ - /security/xccdf_org.ssgproject.content_rule_package_iptables_installed.sh && \ - dnf clean all && \ - rm -rf /security/ /var/cache/dnf/ /var/tmp/* /tmp/* /var/tmp/.???* /tmp/.???* && \ - chmod 755 /usr/local/bin/docker-entrypoint.sh - -ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/docker/parabol-ubi/docker-build/entrypoints/buildenv b/docker/parabol-ubi/docker-build/entrypoints/docker-entrypoint.sh similarity index 100% rename from docker/parabol-ubi/docker-build/entrypoints/buildenv rename to docker/parabol-ubi/docker-build/entrypoints/docker-entrypoint.sh diff --git a/docker/parabol-ubi/docker-build/environments/basic-env b/docker/parabol-ubi/docker-build/environments/basic-env new file mode 100644 index 00000000000..86bbad9252d --- /dev/null +++ b/docker/parabol-ubi/docker-build/environments/basic-env @@ -0,0 +1,17 @@ +FILE_STORE_PROVIDER='local' +HOST='localhost' +NODE_ENV='production' +NODE_EXTRA_CA_CERTS='' +PROTO='https' +PORT='3000' +# Database configurations must be the same used in the docker-build.yml Github workflow +POSTGRES_PASSWORD='temppassword' +POSTGRES_USER='tempuser' +POSTGRES_DB='tempdb' +POSTGRES_HOST='localhost' +POSTGRES_PORT='5432' +REDIS_URL='redis://localhost:6379' +RETHINKDB_SSL='' +RETHINKDB_URL='rethinkdb://localhost:28015/buildDB' +SERVER_ID='0' +SERVER_SECRET='FAKE_VALUE' diff --git a/docker/parabol-ubi/docker-build/environments/buildenv b/docker/parabol-ubi/docker-build/environments/legacy-build similarity index 100% rename from docker/parabol-ubi/docker-build/environments/buildenv rename to docker/parabol-ubi/docker-build/environments/legacy-build diff --git a/docker/parabol-ubi/docker-build/environments/local-buildenv b/docker/parabol-ubi/docker-build/environments/local-buildenv deleted file mode 100644 index f5f7c27ab83..00000000000 --- a/docker/parabol-ubi/docker-build/environments/local-buildenv +++ /dev/null @@ -1,54 +0,0 @@ -ATLASSIAN_CLIENT_ID='' -ATLASSIAN_CLIENT_SECRET='' -AWS_ACCESS_KEY_ID='' -AWS_REGION='' -AWS_S3_BUCKET='' -AWS_SECRET_ACCESS_KEY='' -CDN_BASE_URL='' -FILE_STORE_PROVIDER='local' -GITHUB_CLIENT_ID='' -GITHUB_CLIENT_SECRET='' -GITHUB_WEBHOOK_SECRET='' -GITLAB_CLIENT_ID='' -GITLAB_CLIENT_SECRET='' -GOOGLE_CLOUD_CLIENT_EMAIL='' -GOOGLE_CLOUD_PRIVATE_KEY='' -GOOGLE_CLOUD_PRIVATE_KEY_ID='' -GOOGLE_OAUTH_CLIENT_ID='' -GOOGLE_OAUTH_CLIENT_SECRET='' -GOOGLE_TAG_MANAGER_CONTAINER_ID='' -GRAPHQL_HOST='' -GRAPHQL_PROTOCOL='' -HOST='' -INVITATION_SHORTLINK='' -MAIL_PROVIDER='' -MAIL_GOOGLE_USER='' -MAIL_GOOGLE_PASS='' -MAILGUN_API_KEY='' -MAILGUN_DOMAIN='' -MAILGUN_PUBLIC_KEY='' -MAIL_FROM='' -NODE_ENV='production' -NODE_EXTRA_CA_CERTS='' -PROTO='https' -PGADMIN_DEFAULT_EMAIL='' -PGADMIN_DEFAULT_PASSWORD='' -PGSSLMODE='' -PORT='' -POSTGRES_PASSWORD='temppassword' -POSTGRES_USER='tempuser' -POSTGRES_DB='tempdb' -POSTGRES_HOST='localhost' -POSTGRES_PORT='5432' -REDIS_URL='redis://localhost:6379' -RETHINKDB_SSL='' -RETHINKDB_URL='rethinkdb://localhost:28015/actionProduction' -SENTRY_DSN='' -SERVER_ID='' -SERVER_SECRET='FAKE_VALUE' -SLACK_CLIENT_ID='' -SLACK_CLIENT_SECRET='' -STRIPE_PUBLISHABLE_KEY='' -STRIPE_SECRET_KEY='' -STRIPE_WEBHOOK_SECRET='' -HUBSPOT_API_KEY='' diff --git a/package.json b/package.json index ee9f994f9d9..8749898ad6a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.19.5", + "version": "7.19.7", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/chronos/package.json b/packages/chronos/package.json index f9c5675491e..f44740aee53 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.19.5", + "version": "7.19.7", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -25,6 +25,6 @@ }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.19.5" + "parabol-server": "7.19.7" } } diff --git a/packages/client/package.json b/packages/client/package.json index b5e0427873c..a065260e9ad 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.19.5", + "version": "7.19.7", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/gql-executor/package.json b/packages/gql-executor/package.json index 0e97990948c..ea18a30925c 100644 --- a/packages/gql-executor/package.json +++ b/packages/gql-executor/package.json @@ -1,6 +1,6 @@ { "name": "gql-executor", - "version": "7.19.5", + "version": "7.19.7", "description": "A Stateless GraphQL Executor", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/gqlExecutor#readme", @@ -27,8 +27,8 @@ }, "dependencies": { "dd-trace": "^4.2.0", - "parabol-client": "7.19.5", - "parabol-server": "7.19.5", + "parabol-client": "7.19.7", + "parabol-server": "7.19.7", "undici": "^5.26.2" } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 1b7a9030731..a1780c8c5b4 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -2,7 +2,7 @@ "name": "integration-tests", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.19.5", + "version": "7.19.7", "description": "", "main": "index.js", "scripts": { diff --git a/packages/server/graphql/public/types/Company.ts b/packages/server/graphql/public/types/Company.ts index 2e01f3b8beb..351d4fb7981 100644 --- a/packages/server/graphql/public/types/Company.ts +++ b/packages/server/graphql/public/types/Company.ts @@ -139,7 +139,7 @@ const Company: CompanyResolvers = { const teams = await getTeamsByOrgIds(orgIds, dataLoader, true) const teamIds = teams.map(({id}) => id) if (teamIds.length === 0) return 0 - const filterFn = after ? () => true : (meeting: any) => meeting('createdAt').ge(after) + const filterFn = after ? (meeting: any) => meeting('createdAt').ge(after) : () => true return r .table('NewMeeting') .getAll(r.args(teamIds), {index: 'teamId'}) diff --git a/packages/server/package.json b/packages/server/package.json index 5dc86873eb0..4e47bbb8ad0 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.19.5", + "version": "7.19.7", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -124,7 +124,7 @@ "oauth-1.0a": "^2.2.6", "openai": "^4.24.1", "oy-vey": "^0.11.0", - "parabol-client": "7.19.5", + "parabol-client": "7.19.7", "pg": "^8.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", From 75a567618b3c783c5f3730fba8be2e76ae3f7809 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 08:39:44 -0800 Subject: [PATCH 05/19] chore(release): Test v7.20.0 (#9488) * chore(env vars): Stripe vars moved to the Integrations section (#9427) * chore: fix misleading `isLead` field name on `Team` (#9413) * chore: fix misleading `isLead` field name on `Team` The field indicates whether the viewer is the lead, but when used in a query for a different user, the result could be read wrong. * Fix Team.isLead dependencies * feat: remove team template limit (#9424) * update error message and increase template limit * remove max team template limits * remove canClone prop from CloneTemplate * remove unused threshold * remove unused threshold * feat: Add Google calendar meeting series for recurrence (#9380) * feat: Add recurrence to GCal events * Fun with timezones * fix: Increase the number of projects fetched per request from Atlassian (#9435) We ran into timeouts in `getAllProjects`, presumably because we're doing too many roundtrips. As a quick fix, increse the number of projects fetched per request from 50 to 500. * chore(deps): bump ip from 1.1.8 to 1.1.9 (#9442) Bumps [ip](https://github.com/indutny/node-ip) from 1.1.8 to 1.1.9. - [Commits](https://github.com/indutny/node-ip/compare/v1.1.8...v1.1.9) --- updated-dependencies: - dependency-name: ip dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(release): release v7.17.0 (#9428) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * feat(standalone-deployment): Standalone host deployment improved and documented (#9445) * Docker compose stack improved * Remove unused containers from docker-compse and add useful comment on .env.example about PGSSLMODE * Docker compose profiles added. Documentation extended on how to use the profiles to manage the stack. * README fixed as docker compose up and down commands were not working * Typo fixed and docker-compose command replaced by docker compose * feat: support env-defined saml issuer for PPMIs (#9455) * feat: support env-defined saml issuer for PPMIs Signed-off-by: Matt Krick * feat: support single SAML for entire tenant Signed-off-by: Matt Krick --------- Signed-off-by: Matt Krick * chore: Associate logs with traces (#9444) * chore: Associate logs with traces Add trace information to log output for server side log statements. This does not include logging from code exclusively used for debugging, deploying or development. * Actually add the logger * Fix DD_LOGS_INJECTION check * chore(release): release v7.18.0 (#9450) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * chore: no force-push to prod (#9401) Signed-off-by: Matt Krick * chore(release): release v7.18.1 (#9459) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * feat: embedder service (#9417) * feat: add embedder service --------- Signed-off-by: Matt Krick Co-authored-by: Matt Krick * merge production to avoid force push (#9461) Signed-off-by: Matt Krick * chore(release): release v7.19.0 (#9460) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: checkout prod before merging it (#9463) Signed-off-by: Matt Krick * chore(release): release v7.19.1 (#9464) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: mrege origin/production strategy (#9465) Signed-off-by: Matt Krick * chore(release): release v7.19.2 (#9466) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: force push 5 (#9467) Signed-off-by: Matt Krick * chore(release): release v7.19.3 (#9468) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: limit invites from spammers (#9416) * fix: limit invites from spammers * update where we check pending emails * check total plus pending invites * use invitees instead of pending * fix: Fetch Jira projects in parallel (#9456) Previously we tried to fetch more projects per page, but Jira only ever returns 50 max. Instead once we know how many projects there are after fetching the first page, we fetch all remaining pages in parallel. * fix: replace lone surrogates in draft-js content (#9415) * fix: replace lone surrogates in draft-js content Signed-off-by: Matt Krick * fix typo Signed-off-by: Matt Krick * fix: eslint errors Signed-off-by: Matt Krick --------- Signed-off-by: Matt Krick * chore(deps): bump es5-ext from 0.10.62 to 0.10.64 (#9457) Bumps [es5-ext](https://github.com/medikoo/es5-ext) from 0.10.62 to 0.10.64. - [Release notes](https://github.com/medikoo/es5-ext/releases) - [Changelog](https://github.com/medikoo/es5-ext/blob/main/CHANGELOG.md) - [Commits](https://github.com/medikoo/es5-ext/compare/v0.10.62...v0.10.64) --- updated-dependencies: - dependency-name: es5-ext dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: packages/server/package.json to reduce vulnerabilities (#9434) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-UNDICI-6252336 Co-authored-by: snyk-bot * fix: packages/server/package.json to reduce vulnerabilities (#9392) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-NODEMAILER-6219989 Co-authored-by: snyk-bot * fix: packages/server/package.json to reduce vulnerabilities (#9298) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-FOLLOWREDIRECTS-6141137 Co-authored-by: snyk-bot * chore(deps): bump follow-redirects from 1.14.8 to 1.15.4 (#9312) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.8 to 1.15.4. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.8...v1.15.4) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: Matt Krick Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matt Krick * chore: add upload to GCS step in ironbank (#9471) * add upload to GCS step in ironbank * update workflow name * chore(release): release v7.19.4 (#9470) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: Fix seasonal templates for leap years (#9476) * fix: Fix seasonal templates for leap years It would produce invalid dates on February 29th. * Master was not clean * chore(release): release v7.19.5 (#9477) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix: After parameter for meetingCount was ignored (#9479) * chore(docker-build): simplify the docker build process and reduce docker image size (#9447) * Dockerfile basic created. Improvements added to reduce build time and size (down from 795MB to 445MB, depending on systemtap). Readme reduced, removing the old process used to build the image. * basic-env file using a RethinkDB database name that is clearly dedicated to the building proces. * Readme improved to run all three components * Unused dockerfiles removed. Docker entrypoint renamed. Docker Readme adapted * Legacy build kept in both dockerfile and env file. Readme adapted to use the new basic image. Build GH workflow adapted to use the new basic.dockerfile. * chore(release): release v7.19.6 (#9480) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * fix(docker-build): home folder is /home/node now (#9482) * chore(release): release v7.19.7 (#9483) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> * feat: OpenAIGeneration model for embedder (#9474) * fix: support single-tenant saml record (#9486) Signed-off-by: Matt Krick * chore(release): release v7.20.0 (#9485) Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Signed-off-by: Matt Krick Co-authored-by: Rafa <101704572+rafaelromcar-parabol@users.noreply.github.com> Co-authored-by: Georg Bremer Co-authored-by: Nick O'Ferrall Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> Co-authored-by: Matt Krick Co-authored-by: Jordan Husney Co-authored-by: adaniels-parabol <71724289+adaniels-parabol@users.noreply.github.com> Co-authored-by: snyk-bot Co-authored-by: Dale Bumblis <135627447+dbumblis-parabol@users.noreply.github.com> Co-authored-by: github-actions --- .release-please-manifest.json | 2 +- CHANGELOG.md | 12 +++ package.json | 2 +- packages/chronos/package.json | 4 +- packages/client/package.json | 2 +- packages/embedder/ai_models/AbstractModel.ts | 2 - packages/embedder/ai_models/ModelManager.ts | 9 +- .../embedder/ai_models/OpenAIGeneration.ts | 94 +++++++++++++++++++ .../ai_models/TextEmbeddingsInference.ts | 2 +- .../ai_models/TextGenerationInference.ts | 26 ++--- packages/embedder/embedder.ts | 3 +- .../embedder/indexing/embeddingsTablesOps.ts | 2 +- packages/gql-executor/package.json | 6 +- packages/integration-tests/package.json | 2 +- .../graphql/private/mutations/loginSAML.ts | 7 +- packages/server/package.json | 4 +- packages/server/utils/getSAMLURLFromEmail.ts | 2 +- 17 files changed, 141 insertions(+), 40 deletions(-) create mode 100644 packages/embedder/ai_models/OpenAIGeneration.ts diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 57085bcbc6b..7f814dacbc5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.19.7" + ".": "7.20.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7191952d8a0..9d762695706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.20.0](https://github.com/ParabolInc/parabol/compare/v7.19.7...v7.20.0) (2024-03-01) + + +### Added + +* OpenAIGeneration model for embedder ([#9474](https://github.com/ParabolInc/parabol/issues/9474)) ([807e347](https://github.com/ParabolInc/parabol/commit/807e34718d8a7939b7be84438900ef200a6ca896)) + + +### Fixed + +* support single-tenant saml record ([#9486](https://github.com/ParabolInc/parabol/issues/9486)) ([4e2e2ca](https://github.com/ParabolInc/parabol/commit/4e2e2ca00f237a7a8c94dc2e7f0d2f7d9ef9210d)) + ## [7.19.7](https://github.com/ParabolInc/parabol/compare/v7.19.6...v7.19.7) (2024-02-29) diff --git a/package.json b/package.json index 8749898ad6a..a2b1c1f913c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.19.7", + "version": "7.20.0", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/chronos/package.json b/packages/chronos/package.json index f44740aee53..b0166f09a82 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.19.7", + "version": "7.20.0", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -25,6 +25,6 @@ }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.19.7" + "parabol-server": "7.20.0" } } diff --git a/packages/client/package.json b/packages/client/package.json index a065260e9ad..085bd29d0b3 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.19.7", + "version": "7.20.0", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/embedder/ai_models/AbstractModel.ts b/packages/embedder/ai_models/AbstractModel.ts index b0f709ce485..b57d220cd35 100644 --- a/packages/embedder/ai_models/AbstractModel.ts +++ b/packages/embedder/ai_models/AbstractModel.ts @@ -11,7 +11,6 @@ export interface GenerationModelConfig extends ModelConfig {} export abstract class AbstractModel { public readonly url?: string - public modelInstance: any constructor(config: ModelConfig) { this.url = this.normalizeUrl(config.url) @@ -57,7 +56,6 @@ export interface GenerationOptions { temperature?: number topK?: number topP?: number - truncate?: boolean } export abstract class AbstractGenerationModel extends AbstractModel { diff --git a/packages/embedder/ai_models/ModelManager.ts b/packages/embedder/ai_models/ModelManager.ts index ac8f04cc891..bf6888378c8 100644 --- a/packages/embedder/ai_models/ModelManager.ts +++ b/packages/embedder/ai_models/ModelManager.ts @@ -7,6 +7,7 @@ import { GenerationModelConfig, ModelConfig } from './AbstractModel' +import OpenAIGeneration from './OpenAIGeneration' import TextEmbeddingsInference from './TextEmbeddingsInference' import TextGenerationInference from './TextGenerationInference' @@ -16,7 +17,7 @@ interface ModelManagerConfig { } export type EmbeddingsModelType = 'text-embeddings-inference' -export type GenerationModelType = 'text-generation-inference' +export type GenerationModelType = 'openai' | 'text-generation-inference' export class ModelManager { embeddingModels: AbstractEmbeddingsModel[] @@ -80,9 +81,11 @@ export class ModelManager { const [modelType, _] = modelConfig.model.split(':') as [GenerationModelType, string] switch (modelType) { + case 'openai': { + return new OpenAIGeneration(modelConfig) + } case 'text-generation-inference': { - const generator = new TextGenerationInference(modelConfig) - return generator + return new TextGenerationInference(modelConfig) } default: throw new Error(`unsupported summarization model '${modelType}'`) diff --git a/packages/embedder/ai_models/OpenAIGeneration.ts b/packages/embedder/ai_models/OpenAIGeneration.ts new file mode 100644 index 00000000000..b5614b608c5 --- /dev/null +++ b/packages/embedder/ai_models/OpenAIGeneration.ts @@ -0,0 +1,94 @@ +import OpenAI from 'openai' +import { + AbstractGenerationModel, + GenerationModelConfig, + GenerationModelParams, + GenerationOptions +} from './AbstractModel' + +const MAX_REQUEST_TIME_S = 3 * 60 + +export type ModelId = 'gpt-3.5-turbo-0125' | 'gpt-4-turbo-preview' + +type OpenAIGenerationOptions = Omit + +const modelIdDefinitions: Record = { + 'gpt-3.5-turbo-0125': { + maxInputTokens: 4096 + }, + 'gpt-4-turbo-preview': { + maxInputTokens: 128000 + } +} + +function isValidModelId(object: any): object is ModelId { + return Object.keys(modelIdDefinitions).includes(object) +} + +export class OpenAIGeneration extends AbstractGenerationModel { + private openAIApi: OpenAI | null + private modelId: ModelId + + constructor(config: GenerationModelConfig) { + super(config) + if (!process.env.OPEN_AI_API_KEY) { + this.openAIApi = null + return + } + this.openAIApi = new OpenAI({ + apiKey: process.env.OPEN_AI_API_KEY, + organization: process.env.OPEN_AI_ORG_ID + }) + } + + async summarize(content: string, options: OpenAIGenerationOptions) { + if (!this.openAIApi) { + const eMsg = 'OpenAI is not configured' + console.log('OpenAIGenerationSummarizer.summarize(): ', eMsg) + throw new Error(eMsg) + } + const {maxNewTokens: max_tokens = 512, seed, stop, temperature = 0.8, topP: top_p} = options + const prompt = `Create a brief, one-paragraph summary of the following: ${content}` + + try { + const response = await this.openAIApi.chat.completions.create({ + frequency_penalty: 0, + max_tokens, + messages: [ + { + role: 'user', + content: prompt + } + ], + model: this.modelId, + presence_penalty: 0, + temperature, + seed, + stop, + top_p + }) + const maybeSummary = response.choices[0]?.message?.content?.trim() + if (!maybeSummary) throw new Error('OpenAI returned empty summary') + return maybeSummary + } catch (e) { + console.log('OpenAIGenerationSummarizer.summarize(): ', e) + throw e + } + } + protected constructModelParams(config: GenerationModelConfig): GenerationModelParams { + const modelConfigStringSplit = config.model.split(':') + if (modelConfigStringSplit.length != 2) { + throw new Error('OpenAIGeneration model string must be colon-delimited and len 2') + } + + const maybeModelId = modelConfigStringSplit[1] + if (!isValidModelId(maybeModelId)) + throw new Error(`OpenAIGeneration model id unknown: ${maybeModelId}`) + + this.modelId = maybeModelId + + return modelIdDefinitions[maybeModelId] + } +} + +export default OpenAIGeneration diff --git a/packages/embedder/ai_models/TextEmbeddingsInference.ts b/packages/embedder/ai_models/TextEmbeddingsInference.ts index 93bb2c88c2f..549fadcd6fd 100644 --- a/packages/embedder/ai_models/TextEmbeddingsInference.ts +++ b/packages/embedder/ai_models/TextEmbeddingsInference.ts @@ -63,7 +63,7 @@ export class TextEmbeddingsInference extends AbstractEmbeddingsModel { if (!this.url) throw new Error('TextGenerationInferenceSummarizer model requires url') const maybeModelId = modelConfigStringSplit[1] if (!isValidModelId(maybeModelId)) - throw new Error(`TextGenerationInference model subtype unknown: ${maybeModelId}`) + throw new Error(`TextGenerationInference model id unknown: ${maybeModelId}`) return modelIdDefinitions[maybeModelId] } } diff --git a/packages/embedder/ai_models/TextGenerationInference.ts b/packages/embedder/ai_models/TextGenerationInference.ts index 6f12ce09974..bcf1daa6303 100644 --- a/packages/embedder/ai_models/TextGenerationInference.ts +++ b/packages/embedder/ai_models/TextGenerationInference.ts @@ -25,16 +25,8 @@ export class TextGenerationInference extends AbstractGenerationModel { super(config) } - public async summarize(content: string, options: GenerationOptions) { - const { - maxNewTokens: max_new_tokens = 512, - seed, - stop, - temperature = 0.8, - topP, - topK, - truncate - } = options + async summarize(content: string, options: GenerationOptions) { + const {maxNewTokens: max_new_tokens = 512, seed, stop, temperature = 0.8, topP, topK} = options const parameters = { max_new_tokens, seed, @@ -42,7 +34,7 @@ export class TextGenerationInference extends AbstractGenerationModel { temperature, topP, topK, - truncate + truncate: true } const prompt = `Create a brief, one-paragraph summary of the following: ${content}` const fetchOptions = { @@ -59,27 +51,27 @@ export class TextGenerationInference extends AbstractGenerationModel { } try { - // console.log(`TextGenerationInterface.summarize(): summarizing from ${this.url}/generate`) + // console.log(`TextGenerationInference.summarize(): summarizing from ${this.url}/generate`) const res = await fetchWithRetry(`${this.url}/generate`, fetchOptions) const json = await res.json() if (!json || !json.generated_text) - throw new Error('TextGenerationInterface.summarize(): malformed response') + throw new Error('TextGenerationInference.summarize(): malformed response') return json.generated_text as string } catch (e) { - console.log('TextGenerationInterfaceSummarizer.summarize(): timeout') + console.log('TextGenerationInferenceSummarizer.summarize(): timeout') throw e } } protected constructModelParams(config: GenerationModelConfig): GenerationModelParams { const modelConfigStringSplit = config.model.split(':') if (modelConfigStringSplit.length != 2) { - throw new Error('TextGenerationInterface model string must be colon-delimited and len 2') + throw new Error('TextGenerationInference model string must be colon-delimited and len 2') } - if (!this.url) throw new Error('TextGenerationInterfaceSummarizer model requires url') + if (!this.url) throw new Error('TextGenerationInferenceSummarizer model requires url') const maybeModelId = modelConfigStringSplit[1] if (!isValidModelId(maybeModelId)) - throw new Error(`TextGenerationInterface model subtype unknown: ${maybeModelId}`) + throw new Error(`TextGenerationInference model id unknown: ${maybeModelId}`) return modelIdDefinitions[maybeModelId] } } diff --git a/packages/embedder/embedder.ts b/packages/embedder/embedder.ts index 1072a546d00..f6762013d08 100644 --- a/packages/embedder/embedder.ts +++ b/packages/embedder/embedder.ts @@ -162,9 +162,8 @@ const dequeueAndEmbedUntilEmpty = async (modelManager: ModelManager) => { try { const generator = modelManager.generationModels[0] // use 1st generator if (!generator) throw new Error(`Generator unavailable`) - const summarizeOptions = {maxInputTokens, truncate: true} console.log(`embedder: ...summarizing ${itemKey} for ${modelTable}`) - embedText = await generator.summarize(fullText, summarizeOptions) + embedText = await generator.summarize(fullText, {maxNewTokens: maxInputTokens}) } catch (e) { await updateJobState(jobQueueId, 'failed', { stateMessage: `unable to summarize long embed text: ${e}` diff --git a/packages/embedder/indexing/embeddingsTablesOps.ts b/packages/embedder/indexing/embeddingsTablesOps.ts index b68bc21ccbe..c74eb709708 100644 --- a/packages/embedder/indexing/embeddingsTablesOps.ts +++ b/packages/embedder/indexing/embeddingsTablesOps.ts @@ -52,7 +52,7 @@ export async function selectMetaToQueue( .where(({eb, not, or, and, exists, selectFrom}) => and([ or([ - not(eb('em.models', '<@', sql`ARRAY[${sql.ref('model')}]::varchar[]` as any) as any), + not(eb('em.models', '@>', sql`ARRAY[${sql.ref('model')}]::varchar[]` as any) as any), eb('em.models' as any, 'is', null) ]), not( diff --git a/packages/gql-executor/package.json b/packages/gql-executor/package.json index ea18a30925c..5fc6dbb0961 100644 --- a/packages/gql-executor/package.json +++ b/packages/gql-executor/package.json @@ -1,6 +1,6 @@ { "name": "gql-executor", - "version": "7.19.7", + "version": "7.20.0", "description": "A Stateless GraphQL Executor", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/gqlExecutor#readme", @@ -27,8 +27,8 @@ }, "dependencies": { "dd-trace": "^4.2.0", - "parabol-client": "7.19.7", - "parabol-server": "7.19.7", + "parabol-client": "7.20.0", + "parabol-server": "7.20.0", "undici": "^5.26.2" } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index a1780c8c5b4..9053ded2078 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -2,7 +2,7 @@ "name": "integration-tests", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.19.7", + "version": "7.20.0", "description": "", "main": "index.js", "scripts": { diff --git a/packages/server/graphql/private/mutations/loginSAML.ts b/packages/server/graphql/private/mutations/loginSAML.ts index c14201f35eb..c3137a14205 100644 --- a/packages/server/graphql/private/mutations/loginSAML.ts +++ b/packages/server/graphql/private/mutations/loginSAML.ts @@ -16,6 +16,7 @@ import getSignOnURL from '../../public/mutations/helpers/SAMLHelpers/getSignOnUR import {SSORelayState} from '../../queries/SAMLIdP' import {MutationResolvers} from '../resolverTypes' import standardError from '../../../utils/standardError' +import {isSingleTenantSSO} from '../../../utils/getSAMLURLFromEmail' const serviceProvider = samlify.ServiceProvider({}) samlify.setSchemaValidator(samlXMLValidator) @@ -104,8 +105,10 @@ const loginSAML: MutationResolvers['loginSAML'] = async ( } const ssoDomain = getSSODomainFromEmail(email) if (!ssoDomain || !domains.includes(ssoDomain)) { - // don't blindly trust the IdP - return {error: {message: `${email} does not belong to ${domains.join(', ')}`}} + if (!isSingleTenantSSO) { + // don't blindly trust the IdP unless there is only 1 + return {error: {message: `${email} does not belong to ${domains.join(', ')}`}} + } } if (newMetadata) { diff --git a/packages/server/package.json b/packages/server/package.json index 4e47bbb8ad0..1d09f43fca0 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.19.7", + "version": "7.20.0", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -124,7 +124,7 @@ "oauth-1.0a": "^2.2.6", "openai": "^4.24.1", "oy-vey": "^0.11.0", - "parabol-client": "7.19.7", + "parabol-client": "7.20.0", "pg": "^8.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/packages/server/utils/getSAMLURLFromEmail.ts b/packages/server/utils/getSAMLURLFromEmail.ts index 2c07d74a06d..896a479b483 100644 --- a/packages/server/utils/getSAMLURLFromEmail.ts +++ b/packages/server/utils/getSAMLURLFromEmail.ts @@ -4,7 +4,7 @@ import {URL} from 'url' import {DataLoaderWorker} from '../graphql/graphql' import getKysely from '../postgres/getKysely' -const isSingleTenantSSO = +export const isSingleTenantSSO = process.env.AUTH_INTERNAL_DISABLED === 'true' && process.env.AUTH_GOOGLE_DISABLED === 'true' && process.env.AUTH_MICROSOFT_DISABLED === 'true' && From 5afdbba057fcb207ffe20050929a59ec2b65290a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:45:46 +0100 Subject: [PATCH 06/19] chore(release): Test v7.22.2 (#9546) Signed-off-by: dependabot[bot] Signed-off-by: Matt Krick Co-authored-by: Rafa <101704572+rafaelromcar-parabol@users.noreply.github.com> Co-authored-by: Georg Bremer Co-authored-by: Nick O'Ferrall Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> Co-authored-by: Matt Krick Co-authored-by: Jordan Husney Co-authored-by: adaniels-parabol <71724289+adaniels-parabol@users.noreply.github.com> Co-authored-by: snyk-bot Co-authored-by: Dale Bumblis <135627447+dbumblis-parabol@users.noreply.github.com> Co-authored-by: Bartosz Jarocki Co-authored-by: Marcus Wermuth Co-authored-by: Rafael Romero Co-authored-by: github-actions --- .github/reviewers.yml | 3 - .github/workflows/build.yml | 10 + .github/workflows/migration-order.yml | 2 +- .github/workflows/snyk-yarn-lock-commit.yml | 28 ++ .github/workflows/test.yml | 22 +- .github/workflows/validate_pr_title.yml | 5 +- .release-please-manifest.json | 2 +- .vscode/launch.json | 4 +- CHANGELOG.md | 75 +++++ .../docker-build/environments/pipeline | 2 +- package.json | 2 +- packages/chronos/package.json | 4 +- .../ActivityDetails/ActivityDetails.tsx | 1 + .../ActivityDetailsRecurrenceSettings.tsx | 42 --- .../ActivityDetailsSidebar.tsx | 68 ++--- .../ActivityLibrary/ActivityLibrary.tsx | 145 ++++++---- .../components/ActivityLibrary/Categories.ts | 4 +- .../ActivityLibrary/CreateActivityCard.tsx | 4 +- .../ActivityLibrary/ScheduleMeetingButton.tsx | 70 ++--- .../client/components/AddActivityButton.tsx | 43 --- .../components/AuthenticationDialog.tsx | 3 +- .../client/components/AuthenticationPage.tsx | 4 +- .../client/components/DeleteTeamDialog.tsx | 74 +++++ .../components/DiscussionThreadInput.tsx | 19 +- .../components/EmailPasswordAuthForm.tsx | 21 +- .../components/GenericAuthentication.tsx | 23 +- .../components/GoogleOAuthButtonBlock.tsx | 12 +- .../components/MicrosoftOAuthButtonBlock.tsx | 6 +- .../NewMeetingRecurrenceSettings.tsx | 32 ++- .../client/components/OrgAdminActionMenu.tsx | 93 +++++++ .../Recurrence/RecurrenceSettings.tsx | 90 ++---- .../UpdateRecurrenceSettingsModal.tsx | 85 ++++-- packages/client/components/ScheduleDialog.tsx | 217 +++++++++++++++ .../TeamInvitationEmailCreateAccount.tsx | 5 +- .../components/TeamInvitationEmailSignin.tsx | 3 +- .../TeamInvitationGoogleCreateAccount.tsx | 3 +- .../components/TeamInvitationWrapper.tsx | 29 +- packages/client/modules/demo/DemoUser.ts | 3 +- packages/client/modules/demo/initDB.ts | 1 - .../EmailDiscussionMentioned.tsx | 4 +- .../EmailResponseReplied.tsx | 4 +- .../summary/components/NewMeetingSummary.tsx | 2 +- .../components/ProviderList/ProviderList.tsx | 6 +- .../components/GcalModal/GcalSettings.tsx | 164 +++++++++++ .../OrgAuthenticationMetadata.tsx | 2 +- .../components/OrgBilling/Organization.tsx | 22 +- .../components/OrgMembers/OrgMembers.tsx | 5 + .../OrgTeamMembers/OrgTeamMemberMenu.tsx | 71 +++++ .../OrgTeamMembers/OrgTeamMembers.tsx | 104 +++++++ .../OrgTeamMembers/OrgTeamMembersMenu.tsx | 26 ++ .../OrgTeamMembers/OrgTeamMembersRoot.tsx | 25 ++ .../OrgTeamMembers/OrgTeamMembersRow.tsx | 117 ++++++++ .../components/OrgTeams/OrgTeams.tsx | 57 ++-- .../components/OrgTeams/OrgTeamsRow.tsx | 51 ++-- .../components/OrgUserRow/OrgMemberRow.tsx | 258 ++++++++++++------ .../containers/OrgMembers/OrgMembersRoot.tsx | 1 + .../client/mutations/ArchiveTeamMutation.ts | 8 + packages/client/package.json | 7 +- packages/client/serviceWorker/sw.ts | 2 +- .../styles/theme/images/anonymous-avatar.png | Bin 0 -> 1843 bytes .../client/ui/AlertDialog/AlertDialog.tsx | 5 + .../ui/AlertDialog/AlertDialogAction.tsx | 18 ++ .../ui/AlertDialog/AlertDialogCancel.tsx | 18 ++ .../ui/AlertDialog/AlertDialogContent.tsx | 23 ++ .../ui/AlertDialog/AlertDialogDescription.tsx | 15 + .../ui/AlertDialog/AlertDialogFooter.tsx | 10 + .../ui/AlertDialog/AlertDialogHeader.tsx | 7 + .../ui/AlertDialog/AlertDialogOverlay.tsx | 18 ++ .../ui/AlertDialog/AlertDialogPortal.tsx | 3 + .../ui/AlertDialog/AlertDialogTitle.tsx | 15 + .../ui/AlertDialog/AlertDialogTrigger.tsx | 3 + packages/client/ui/Avatar/Avatar.tsx | 16 ++ packages/client/ui/Avatar/AvatarFallback.tsx | 19 ++ packages/client/ui/Avatar/AvatarImage.tsx | 16 ++ packages/client/ui/Button/Button.tsx | 21 +- packages/client/utils/GoogleClientManager.ts | 9 +- .../client/utils/MicrosoftClientManager.ts | 9 +- .../client/utils/getOAuthPopupFeatures.ts | 7 +- packages/client/utils/getTokenFromSSO.ts | 4 +- packages/client/utils/makeCheckinGreeting.ts | 44 ++- packages/gql-executor/package.json | 6 +- packages/integration-tests/package.json | 2 +- packages/server/__tests__/globalSetup.ts | 2 +- .../helpers/removeTeamsLimitObjects.ts | 17 +- .../server/billing/helpers/teamLimitsCheck.ts | 20 +- .../database/types/FailedAuthRequest.ts | 7 +- .../server/database/types/ScheduledJob.ts | 3 - .../database/types/scheduleTeamLimitsJobs.ts | 20 +- .../server/fileStorage/FileStoreManager.ts | 6 +- packages/server/fileStorage/GCSManager.ts | 12 +- .../server/fileStorage/S3FileStoreManager.ts | 12 +- .../graphql/mutations/helpers/attemptLogin.ts | 52 ++-- .../mutations/helpers/removeScheduledJobs.ts | 19 +- .../server/graphql/mutations/resetPassword.ts | 4 +- .../server/graphql/mutations/setStageTimer.ts | 10 +- .../private/mutations/addNewFeature.ts | 6 +- .../graphql/private/mutations/loginSAML.ts | 6 +- .../private/mutations/runScheduledJobs.ts | 22 +- .../typeDefs/updateOrgFeatureFlag.graphql | 1 - .../public/mutations/addReactjiToReactable.ts | 34 +-- .../public/mutations/updateFeatureFlag.ts | 14 +- packages/server/graphql/public/permissions.ts | 3 +- .../server/graphql/public/queries/SAMLIdP.ts | 13 + .../public/typeDefs/Organization.graphql | 1 - .../graphql/public/typeDefs/Query.graphql | 4 +- .../graphql/public/typeDefs/_legacy.graphql | 2 +- .../public/typeDefs/startCheckIn.graphql | 2 +- .../typeDefs/startRetrospective.graphql | 2 +- .../public/typeDefs/startTeamPrompt.graphql | 2 +- .../public/typeDefs/updateFeatureFlag.graphql | 2 - .../public/types/OrganizationFeatureFlags.ts | 1 - .../public/types/StartRetrospectiveSuccess.ts | 3 +- .../graphql/public/types/UserFeatureFlags.ts | 1 - packages/server/graphql/queries/SAMLIdP.ts | 33 --- packages/server/graphql/rootQuery.ts | 4 +- packages/server/graphql/types/Organization.ts | 17 +- packages/server/graphql/types/UserFlagEnum.ts | 1 - packages/server/initPublicPath.ts | 16 ++ packages/server/jiraImagesHandler.ts | 11 +- packages/server/package.json | 10 +- .../1693991480688_truncateReflectPromptIds.ts | 9 +- .../migrations/1694191002164_migrateSAML.ts | 6 +- ...ameEmailVerificationSegmentIdToPseudoId.ts | 6 +- .../1709551949071_makeAllTemplatesFree.ts | 20 ++ .../1709927369000_addFailedAuthRequest.ts | 28 ++ .../1709927822000_addScheduledJob.ts | 38 +++ .../1709927835000_moveScheduledJob.ts | 59 ++++ .../postgres/queries/archiveTeamsByTeamIds.ts | 17 -- .../queries/src/addUserNewFeatureQuery.sql | 6 - .../src/appendTeamResponseReactjiQuery.sql | 7 - .../src/appendUserFeatureFlagsQuery.sql | 7 - .../queries/src/appendUserTmsQuery.sql | 6 - .../src/archiveTeamsByTeamIdsQuery.sql | 10 - .../src/removeTeamResponseReactjiQuery.sql | 7 - .../src/removeUserFeatureFlagsQuery.sql | 12 - .../server/safeMutations/addTeamIdToTMS.ts | 11 +- .../server/safeMutations/safeArchiveTeam.ts | 12 +- packages/server/selfHostedHandler.ts | 2 +- packages/server/types/modules.d.ts | 2 +- packages/server/utils/authorization.ts | 25 +- packages/server/utils/getSAMLURLFromEmail.ts | 8 +- packages/server/utils/serveStatic.ts | 6 +- packages/server/utils/uwsGetIP.ts | 2 +- .../toolboxSrc/applyEnvVarsToClientAssets.ts | 27 +- scripts/toolboxSrc/pushToCDN.ts | 73 +++-- scripts/webpack/dev.servers.config.js | 9 +- scripts/webpack/prod.client.config.js | 2 +- scripts/webpack/prod.servers.config.js | 57 +--- .../webpack/utils/node-loader-private/cjs.js | 6 + .../utils/node-loader-private/index.js | 36 +++ .../utils/node-loader-private/options.json | 20 ++ yarn.lock | 224 ++++++++++++--- 152 files changed, 2632 insertions(+), 1006 deletions(-) create mode 100644 .github/workflows/snyk-yarn-lock-commit.yml delete mode 100644 packages/client/components/ActivityLibrary/ActivityDetailsRecurrenceSettings.tsx delete mode 100644 packages/client/components/AddActivityButton.tsx create mode 100644 packages/client/components/DeleteTeamDialog.tsx create mode 100644 packages/client/components/OrgAdminActionMenu.tsx create mode 100644 packages/client/components/ScheduleDialog.tsx create mode 100644 packages/client/modules/userDashboard/components/GcalModal/GcalSettings.tsx create mode 100644 packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMemberMenu.tsx create mode 100644 packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembers.tsx create mode 100644 packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersMenu.tsx create mode 100644 packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRoot.tsx create mode 100644 packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRow.tsx create mode 100644 packages/client/styles/theme/images/anonymous-avatar.png create mode 100644 packages/client/ui/AlertDialog/AlertDialog.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogAction.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogCancel.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogContent.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogDescription.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogFooter.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogHeader.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogOverlay.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogPortal.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogTitle.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogTrigger.tsx create mode 100644 packages/client/ui/Avatar/Avatar.tsx create mode 100644 packages/client/ui/Avatar/AvatarFallback.tsx create mode 100644 packages/client/ui/Avatar/AvatarImage.tsx create mode 100644 packages/server/graphql/public/queries/SAMLIdP.ts delete mode 100644 packages/server/graphql/queries/SAMLIdP.ts create mode 100644 packages/server/initPublicPath.ts create mode 100644 packages/server/postgres/migrations/1709551949071_makeAllTemplatesFree.ts create mode 100644 packages/server/postgres/migrations/1709927369000_addFailedAuthRequest.ts create mode 100644 packages/server/postgres/migrations/1709927822000_addScheduledJob.ts create mode 100644 packages/server/postgres/migrations/1709927835000_moveScheduledJob.ts delete mode 100644 packages/server/postgres/queries/archiveTeamsByTeamIds.ts delete mode 100644 packages/server/postgres/queries/src/addUserNewFeatureQuery.sql delete mode 100644 packages/server/postgres/queries/src/appendTeamResponseReactjiQuery.sql delete mode 100644 packages/server/postgres/queries/src/appendUserFeatureFlagsQuery.sql delete mode 100644 packages/server/postgres/queries/src/appendUserTmsQuery.sql delete mode 100644 packages/server/postgres/queries/src/archiveTeamsByTeamIdsQuery.sql delete mode 100644 packages/server/postgres/queries/src/removeTeamResponseReactjiQuery.sql delete mode 100644 packages/server/postgres/queries/src/removeUserFeatureFlagsQuery.sql create mode 100644 scripts/webpack/utils/node-loader-private/cjs.js create mode 100644 scripts/webpack/utils/node-loader-private/index.js create mode 100644 scripts/webpack/utils/node-loader-private/options.json diff --git a/.github/reviewers.yml b/.github/reviewers.yml index 2b92d6ef8f6..8d2732ba714 100644 --- a/.github/reviewers.yml +++ b/.github/reviewers.yml @@ -1,19 +1,16 @@ reviewers: groups: reviewers: - - igorlesnenko - nickoferrall maintainers: - mattkrick - Dschoordsch data: - tianrunhe - - tghanken designers: - ackernaut devops: - rafaelromcar-parabol - - adaniels-parabol - dbumblis-parabol none: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e2e74b3df6..a19c552ac06 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,7 @@ on: push: branches: - "release-please--**" + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true @@ -137,3 +138,12 @@ jobs: --service=parabol-saas-production \ --release-version=${{env.ACTION_VERSION}} \ --minified-path-prefix=${{env.CDN_BUILD_URL}} + + - name: Report Status + if: failure() + uses: ravsamhq/notify-slack-action@v2 + with: + status: ${{ job.status }} + notify_when: 'failure' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_GH_ACTIONS_NOTIFICATIONS }} diff --git a/.github/workflows/migration-order.yml b/.github/workflows/migration-order.yml index a026ac8892f..9bce03582a5 100644 --- a/.github/workflows/migration-order.yml +++ b/.github/workflows/migration-order.yml @@ -10,7 +10,7 @@ jobs: - name: Checkout master uses: actions/checkout@v3 with: - ref: master + ref: ${{ github.base_ref }} - name: Get newest migration on master run: | diff --git a/.github/workflows/snyk-yarn-lock-commit.yml b/.github/workflows/snyk-yarn-lock-commit.yml new file mode 100644 index 00000000000..f4705ec4b79 --- /dev/null +++ b/.github/workflows/snyk-yarn-lock-commit.yml @@ -0,0 +1,28 @@ +name: Update Snyk PR to add yarn.lock + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + update-snyk-pr: + if: contains(github.event.pull_request.title, '[Snyk]') + runs-on: ubuntu-latest + + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, no need to create one + + - name: Install dependencies + run: yarn install + + - name: Commit yarn.lock to the PR branch + run: | + git config --global user.email "action@github.com" + git config --global user.name "GitHub Action" + git add yarn.lock + git commit -m "Update yarn.lock" || echo "No changes to commit" + git push diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a4cc2323d8c..cb6c94bd9a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -93,11 +93,12 @@ jobs: run: yarn predeploy - name: Start testing server in background - run: | - yarn start & - - - name: Wait for testing server to be healthy - run: curl -4 --retry 30 --retry-connrefused --retry-delay 2 http://localhost:3000/graphql + uses: JarvusInnovations/background-action@v1 + with: + run: | + yarn start & + wait-on: | + http://localhost:3000/graphql - name: Run server tests run: yarn test:server -- --reporters=default --reporters=jest-junit @@ -130,5 +131,14 @@ jobs: uses: actions/upload-artifact@v2 with: name: test-results - path: test-results/ + path: packages/integration-tests/test-results/ retention-days: 7 + + - name: Report Status + if: ${{ failure() && github.ref_name == 'master' }} + uses: ravsamhq/notify-slack-action@v2 + with: + status: ${{ job.status }} + notify_when: 'failure' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_GH_ACTIONS_NOTIFICATIONS }} diff --git a/.github/workflows/validate_pr_title.yml b/.github/workflows/validate_pr_title.yml index 629ae6230e4..1e5a2d91a2a 100644 --- a/.github/workflows/validate_pr_title.yml +++ b/.github/workflows/validate_pr_title.yml @@ -14,9 +14,6 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@v4 + - uses: amannn/action-semantic-pull-request@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - # See https://github.com/marketplace/actions/semantic-pull-request for details on other available options - validateSingleCommit: true diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7f814dacbc5..a5e4e701ea7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.20.0" + ".": "7.22.2" } diff --git a/.vscode/launch.json b/.vscode/launch.json index 2d3b6382f4e..a7fe78cf299 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,7 +30,7 @@ { "name": "Launch Chrome", "request": "launch", - "type": "pwa-chrome", + "type": "chrome", "url": "http://localhost:3000", "webRoot": "${workspaceFolder}" }, @@ -38,7 +38,7 @@ "type": "node", "request": "launch", "name": "Debug Executor", - "program": "scripts/runExecutor.js", + "program": "scripts/runExecutor.js" }, { "type": "node", diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d762695706..e40f96936c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,81 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.22.2](https://github.com/ParabolInc/parabol/compare/v7.22.1...v7.22.2) (2024-03-18) + + +### Fixed + +* Only read the first ip of the x-forwarded-for header ([#9545](https://github.com/ParabolInc/parabol/issues/9545)) ([081f7a0](https://github.com/ParabolInc/parabol/commit/081f7a09a94a3ce2d23816e801c745040737364f)) +* **snyk-ci:** removed toLowerCase function as it does not exit ([2c98ca1](https://github.com/ParabolInc/parabol/commit/2c98ca1c71bb223d736a9d259f1d6314b8579c35)) +* use base ref for migrition order check ([#9542](https://github.com/ParabolInc/parabol/issues/9542)) ([0217e11](https://github.com/ParabolInc/parabol/commit/0217e11147201759db85a1df010bc3d4d291b202)) + + +### Changed + +* add GH Action, on Snyk PRs commit yarn.lock ([#9534](https://github.com/ParabolInc/parabol/issues/9534)) ([bd907a9](https://github.com/ParabolInc/parabol/commit/bd907a915a1848b17ff9385c70de072390f54cf5)) + +## [7.22.1](https://github.com/ParabolInc/parabol/compare/v7.22.0...v7.22.1) (2024-03-14) + + +### Fixed + +* node-loader that ignores public path ([#9537](https://github.com/ParabolInc/parabol/issues/9537)) ([1009ede](https://github.com/ParabolInc/parabol/commit/1009edefba19b1caec0b8f9708aa468d565fc225)) + + +### Changed + +* migrate FailedAuthRequest to pg ([#9500](https://github.com/ParabolInc/parabol/issues/9500)) ([efc0dc9](https://github.com/ParabolInc/parabol/commit/efc0dc9d090f2bcd03d5abedc04a5507addb2f6e)) +* migrate ScheduledJob from rethinkdb to pg ([#9490](https://github.com/ParabolInc/parabol/issues/9490)) ([5c39fde](https://github.com/ParabolInc/parabol/commit/5c39fde04c6e8b5c31d70258d4ef7f548aa28298)) + +## [7.22.0](https://github.com/ParabolInc/parabol/compare/v7.21.0...v7.22.0) (2024-03-13) + + +### Added + +* Add team sections to the Custom category in activity library ([#9511](https://github.com/ParabolInc/parabol/issues/9511)) ([2338414](https://github.com/ParabolInc/parabol/commit/233841498bf997343f3d94e443104973078bf736)) +* added additinal check-in questions ([10c6f69](https://github.com/ParabolInc/parabol/commit/10c6f6932008fcca434d1b6a73c288aea88768d5)) +* managing teams ([#9285](https://github.com/ParabolInc/parabol/issues/9285)) ([f351cf9](https://github.com/ParabolInc/parabol/commit/f351cf9f5a894fe019f331cc0ec6f012a0779c42)) +* Recurring GCal event dialog ([#9506](https://github.com/ParabolInc/parabol/issues/9506)) ([fc4429c](https://github.com/ParabolInc/parabol/commit/fc4429c85dd9610d3fdadf83882c2dbdd88f424f)) +* Release MS Teams integration ([#9527](https://github.com/ParabolInc/parabol/issues/9527)) ([1ed2796](https://github.com/ParabolInc/parabol/commit/1ed279673fdaa7a21a995677c7e2b0e6a7c41f96)) + + +### Fixed + +* Korean greeting corrected ([#9525](https://github.com/ParabolInc/parabol/issues/9525)) ([10c6f69](https://github.com/ParabolInc/parabol/commit/10c6f6932008fcca434d1b6a73c288aea88768d5)) +* Make hasGCalError optional ([#9526](https://github.com/ParabolInc/parabol/issues/9526)) ([9350b93](https://github.com/ParabolInc/parabol/commit/9350b93b7a2a6f48e0af712cc0a6edbb8395004c)) +* recreate lockfile ([#9516](https://github.com/ParabolInc/parabol/issues/9516)) ([af47966](https://github.com/ParabolInc/parabol/commit/af47966d6c07b295536327a3ee4d6bac1fece57b)) + + +### Changed + +* **ci:** add capability to manually generate Docker Images ([#9524](https://github.com/ParabolInc/parabol/issues/9524)) ([88bf97f](https://github.com/ParabolInc/parabol/commit/88bf97f6ff3e820d49a24e9a8a8cf4dbab46b22c)) +* **gh-actions:** reporting status to Slack if test or build GH Actions fail ([#9512](https://github.com/ParabolInc/parabol/issues/9512)) ([e7539d1](https://github.com/ParabolInc/parabol/commit/e7539d152ccfb5fbe12bdcb9b5ce3cc64fd2955c)) +* Remove Add Activity button from discussions ([#9528](https://github.com/ParabolInc/parabol/issues/9528)) ([37bd20c](https://github.com/ParabolInc/parabol/commit/37bd20cf8e073d353e3b3dffb5f3037c199adf67)) + +## [7.21.0](https://github.com/ParabolInc/parabol/compare/v7.20.0...v7.21.0) (2024-03-06) + + +### Added + +* make all templates free ([#9503](https://github.com/ParabolInc/parabol/issues/9503)) ([6762ebc](https://github.com/ParabolInc/parabol/commit/6762ebc4068f9062091db7f8d467fecf60388d63)) +* saml login no email, auth design fixups ([#9507](https://github.com/ParabolInc/parabol/issues/9507)) ([4ce391e](https://github.com/ParabolInc/parabol/commit/4ce391e53a11cbd174bf67364db29128afe72092)) + + +### Fixed + +* upgrade graphql-jit from 0.7.4 to 0.8.4 ([#9495](https://github.com/ParabolInc/parabol/issues/9495)) ([fe1ad43](https://github.com/ParabolInc/parabol/commit/fe1ad434a990ffbfb5c52863988e7f4406d2ac84)) +* upgrade oy-vey from 0.11.2 to 0.12.1 ([#9497](https://github.com/ParabolInc/parabol/issues/9497)) ([1751731](https://github.com/ParabolInc/parabol/commit/17517318502b5aaa0849c8d03c4e068f5da92e82)) +* upgrade sharp from 0.32.6 to 0.33.2 ([#9493](https://github.com/ParabolInc/parabol/issues/9493)) ([9fff933](https://github.com/ParabolInc/parabol/commit/9fff93397e35e2bfeb8f4fbc34c8d535284551eb)) + + +### Changed + +* bump ts node ([#9498](https://github.com/ParabolInc/parabol/issues/9498)) ([58c5817](https://github.com/ParabolInc/parabol/commit/58c5817463bcb73dbcaa83b05a0d2a201262de77)) +* put server assets on CDN ([#9278](https://github.com/ParabolInc/parabol/issues/9278)) ([06c1f7e](https://github.com/ParabolInc/parabol/commit/06c1f7eec63c6de343181ee1324635c2ae8d286a)) +* remove pg-typed part 1 ([#9508](https://github.com/ParabolInc/parabol/issues/9508)) ([5dfe26b](https://github.com/ParabolInc/parabol/commit/5dfe26b29753f881dc54c35d6dfa15e894f3726a)) +* Update reviewers ([#9504](https://github.com/ParabolInc/parabol/issues/9504)) ([a95fb88](https://github.com/ParabolInc/parabol/commit/a95fb88b9a76e04eb73630404e29e6325dcf1a12)) + ## [7.20.0](https://github.com/ParabolInc/parabol/compare/v7.19.7...v7.20.0) (2024-03-01) diff --git a/docker/parabol-ubi/docker-build/environments/pipeline b/docker/parabol-ubi/docker-build/environments/pipeline index 8fd2ac929d9..5641d43725f 100644 --- a/docker/parabol-ubi/docker-build/environments/pipeline +++ b/docker/parabol-ubi/docker-build/environments/pipeline @@ -31,7 +31,7 @@ MAILGUN_PUBLIC_KEY='' MAIL_FROM='' NODE_ENV='production' NODE_EXTRA_CA_CERTS='' -PROTO='https' +PROTO='http' PGADMIN_DEFAULT_EMAIL='' PGADMIN_DEFAULT_PASSWORD='' PGSSLMODE='' diff --git a/package.json b/package.json index a2b1c1f913c..d228664dd0c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.20.0", + "version": "7.22.2", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/chronos/package.json b/packages/chronos/package.json index b0166f09a82..cc50069e63f 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.20.0", + "version": "7.22.2", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -25,6 +25,6 @@ }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.20.0" + "parabol-server": "7.22.2" } } diff --git a/packages/client/components/ActivityLibrary/ActivityDetails/ActivityDetails.tsx b/packages/client/components/ActivityLibrary/ActivityDetails/ActivityDetails.tsx index 9fcca27d12c..7f1706645c2 100644 --- a/packages/client/components/ActivityLibrary/ActivityDetails/ActivityDetails.tsx +++ b/packages/client/components/ActivityLibrary/ActivityDetails/ActivityDetails.tsx @@ -74,6 +74,7 @@ const ActivityDetails = (props: Props) => { if (!activity) { return } + // eslint-disable react-hooks/rules-of-hooks -- return above violates these rules, but is just a safeguard and not normal usage useEffect(() => { SendClientSideEvent(atmosphere, 'Viewed Template', { meetingType: activity.type, diff --git a/packages/client/components/ActivityLibrary/ActivityDetailsRecurrenceSettings.tsx b/packages/client/components/ActivityLibrary/ActivityDetailsRecurrenceSettings.tsx deleted file mode 100644 index 7fe8efeeb21..00000000000 --- a/packages/client/components/ActivityLibrary/ActivityDetailsRecurrenceSettings.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react' -import {RecurrenceSettings} from '../Recurrence/RecurrenceSettings' -import NewMeetingDropdown from '../NewMeetingDropdown' -import {toHumanReadable} from '../Recurrence/HumanReadableRecurrenceRule' -import useModal from '../../hooks/useModal' -import DialogContainer from '../DialogContainer' - -interface Props { - onRecurrenceSettingsUpdated: (recurrenceSettings: RecurrenceSettings) => void - recurrenceSettings: RecurrenceSettings - placeholder: string -} - -export const ActivityDetailsRecurrenceSettings = (props: Props) => { - const {onRecurrenceSettingsUpdated, recurrenceSettings, placeholder} = props - const {togglePortal, modalPortal} = useModal({ - id: 'activityDetailsRecurrenceSettings' - }) - - return ( - <> - - {modalPortal( - - - - )} - - ) -} diff --git a/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx b/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx index 645fd319fab..a686ca89c36 100644 --- a/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx +++ b/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx @@ -20,10 +20,12 @@ import SendClientSideEvent from '../../utils/SendClientSideEvent' import StartCheckInMutation from '../../mutations/StartCheckInMutation' import StartTeamPromptMutation from '../../mutations/StartTeamPromptMutation' import {PALETTE} from '../../styles/paletteV3' -import {CreateGcalEventInput} from '../../__generated__/StartRetrospectiveMutation.graphql' +import { + CreateGcalEventInput, + RecurrenceSettingsInput +} from '../../__generated__/StartRetrospectiveMutation.graphql' import sortByTier from '../../utils/sortByTier' import {MeetingTypeEnum} from '../../__generated__/ActivityDetailsQuery.graphql' -import {RecurrenceSettings} from '../Recurrence/RecurrenceSettings' import NewMeetingSettingsToggleAnonymity from '../NewMeetingSettingsToggleAnonymity' import NewMeetingSettingsToggleTeamHealth from '../NewMeetingSettingsToggleTeamHealth' import NewMeetingSettingsToggleCheckIn from '../NewMeetingSettingsToggleCheckIn' @@ -32,7 +34,6 @@ import FlatPrimaryButton from '../FlatPrimaryButton' import NewMeetingActionsCurrentMeetings from '../NewMeetingActionsCurrentMeetings' import RaisedButton from '../RaisedButton' import NewMeetingTeamPicker from '../NewMeetingTeamPicker' -import {ActivityDetailsRecurrenceSettings} from './ActivityDetailsRecurrenceSettings' import {AdhocTeamMultiSelect, Option} from '../AdhocTeamMultiSelect/AdhocTeamMultiSelect' import {Select} from '../../ui/Select/Select' import {SelectTrigger} from '../../ui/Select/SelectTrigger' @@ -119,17 +120,13 @@ const ActivityDetailsSidebar = (props: Props) => { ...NewMeetingTeamPicker_teams ...NewMeetingActionsCurrentMeetings_team ...ScheduleMeetingButton_team + ...ScheduleDialog_team } `, teamsRef ) const atmosphere = useAtmosphere() - const [recurrenceSettings, setRecurrenceSettings] = useState({ - name: '', - rrule: null - }) - const templateTeam = teams.find((team) => team.id === selectedTemplate.teamId) const availableTeams = @@ -193,7 +190,10 @@ const ActivityDetailsSidebar = (props: Props) => { } : null - const handleStartActivity = (gcalInput?: CreateGcalEventInput) => { + const handleStartActivity = ( + gcalInput?: CreateGcalEventInput, + recurrenceSettings?: RecurrenceSettingsInput + ) => { if (submitting) return submitMutation() if (type === 'teamPrompt') { @@ -201,10 +201,12 @@ const ActivityDetailsSidebar = (props: Props) => { atmosphere, { teamId: selectedTeam.id, - recurrenceSettings: { - rrule: recurrenceSettings.rrule?.toString(), - name: recurrenceSettings.name - }, + recurrenceSettings: recurrenceSettings + ? { + rrule: recurrenceSettings.rrule?.toString(), + name: recurrenceSettings.name + } + : undefined, gcalInput }, {history, onError, onCompleted} @@ -238,10 +240,12 @@ const ActivityDetailsSidebar = (props: Props) => { atmosphere, { teamId: selectedTeam.id, - recurrenceSettings: { - rrule: recurrenceSettings.rrule?.toString(), - name: recurrenceSettings.name - }, + recurrenceSettings: recurrenceSettings + ? { + rrule: recurrenceSettings.rrule?.toString(), + name: recurrenceSettings.name + } + : undefined, gcalInput }, {history, onError, onCompleted} @@ -299,6 +303,20 @@ const ActivityDetailsSidebar = (props: Props) => { history.push(`/me/organizations/${selectedTeam.orgId}/billing`) } + const meetingNamePlaceholder = + type === 'retrospective' + ? 'Retro' + : type === 'teamPrompt' + ? 'Standup' + : type === 'poker' + ? 'Poker' + : type === 'action' + ? 'Check-in' + : 'Meeting' + const withRecurrence = + type === 'teamPrompt' || + (selectedTeam.organization.featureFlags.recurringRetros && type === 'retrospective') + return ( <> {isOpen &&
} @@ -404,13 +422,6 @@ const ActivityDetailsSidebar = (props: Props) => { teamRef={selectedTeam} /> - {selectedTeam.organization.featureFlags.recurringRetros && ( - - )} )} {type === 'poker' && ( @@ -419,13 +430,6 @@ const ActivityDetailsSidebar = (props: Props) => { {type === 'action' && ( )} - {type === 'teamPrompt' && ( - - )} )}
@@ -449,6 +453,8 @@ const ActivityDetailsSidebar = (props: Props) => { handleStartActivity={handleStartActivity} mutationProps={mutationProps} teamRef={selectedTeam} + placeholder={meetingNamePlaceholder} + withRecurrence={withRecurrence} /> )} diff --git a/packages/client/components/ActivityLibrary/ActivityLibrary.tsx b/packages/client/components/ActivityLibrary/ActivityLibrary.tsx index 0b9a4f8d215..921b2bb7b48 100644 --- a/packages/client/components/ActivityLibrary/ActivityLibrary.tsx +++ b/packages/client/components/ActivityLibrary/ActivityLibrary.tsx @@ -1,7 +1,7 @@ import * as ScrollArea from '@radix-ui/react-scroll-area' import graphql from 'babel-plugin-relay/macro' import clsx from 'clsx' -import React, {useEffect, useMemo} from 'react' +import React, {Fragment, useEffect, useMemo} from 'react' import {PreloadedQuery, commitLocalUpdate, usePreloadedQuery} from 'react-relay' import {Redirect} from 'react-router' import {Link} from 'react-router-dom' @@ -134,17 +134,63 @@ const CategoryIDToColorClass = Object.fromEntries( export type Template = Omit -type SubCategory = 'popular' | 'recentlyUsed' | 'recentlyUsedInOrg' | 'neverTried' | 'getStarted' - -const subCategoryMapping: Record = { +const MAX_PER_SUBCATEGORY = 6 +const subCategoryMapping = { popular: 'Popular templates', recentlyUsed: 'You used these recently', recentlyUsedInOrg: 'Others in your organization are using', neverTried: 'Try these activities', - getStarted: 'Activities to get you started' + getStarted: 'Activities to get you started', + other: 'Other activities' +} as const + +const mapRetroSubCategories = (templates: readonly Template[]) => { + const subCategoryTemplates = Object.fromEntries( + Object.keys(subCategoryMapping).map((subCategory) => { + return [ + subCategory, + templates + .filter((template) => template.subCategories?.includes(subCategory)) + .slice(0, MAX_PER_SUBCATEGORY) as Template[] + ] + }) + ) + //just in case there were values already + subCategoryTemplates.other = [] + subCategoryTemplates.other = templates.filter( + (template) => + !Object.values(subCategoryTemplates) + .flat() + .find((catTemplate) => catTemplate.id === template.id) + ) + + return Object.fromEntries( + Object.entries(subCategoryTemplates).map(([key, value]) => { + return [subCategoryMapping[key as keyof typeof subCategoryMapping]!, value] + }) + ) } -const MAX_PER_SUBCATEGORY = 6 +const mapTeamCategories = (templates: readonly Template[]) => { + // list public templates last + const publicTemplates = [] as Template[] + const mapped = templates.reduce((acc, template) => { + const {team, scope} = template + if (scope === 'PUBLIC') { + publicTemplates.push(template) + } else { + const {name} = team + if (!acc[name]) { + acc[name] = [] + } + acc[name]!.push(template) + } + return acc + }, {} as Record) + + mapped['Parabol'] = publicTemplates + return mapped +} export const ActivityLibrary = (props: Props) => { const atmosphere = useAtmosphere() @@ -209,23 +255,24 @@ export const ActivityLibrary = (props: Props) => { ) }, [searchQuery, filteredTemplates, categoryId]) - const subCategoryTemplates = Object.fromEntries( - Object.keys(subCategoryMapping).map((subCategory) => { - return [ - subCategory, - templatesToRender - .filter((template) => template.subCategories?.includes(subCategory)) - .slice(0, MAX_PER_SUBCATEGORY) as Template[] - ] - }) - ) as Record + const sectionedTemplates = useMemo(() => { + // Show the teams on search as well, because you can search by team name + if (categoryId === CUSTOM_CATEGORY_ID || searchQuery.length > 0) { + return mapTeamCategories(templatesToRender) + } - const otherTemplates = templatesToRender.filter( - (template) => - !Object.values(subCategoryTemplates) - .flat() - .find((catTemplate) => catTemplate.id === template.id) - ) as Template[] + if (searchQuery.length > 0) { + return undefined + } + + if (categoryId === 'retrospective') { + return mapRetroSubCategories(templatesToRender) + } + if (categoryId === 'recommended') { + return {[subCategoryMapping.getStarted]: [...templatesToRender]} + } + return undefined + }, [categoryId, templatesToRender]) if (!featureFlags.retrosInDisguise) { return @@ -235,10 +282,6 @@ export const ActivityLibrary = (props: Props) => { return } - const selectedCategory = categoryId as CategoryID | typeof QUICK_START_CATEGORY_ID - const quickStartTitle = - selectedCategory === 'recommended' ? subCategoryMapping['getStarted'] : undefined - return (
@@ -272,7 +315,7 @@ export const ActivityLibrary = (props: Props) => { )} Create custom activity @@ -297,7 +340,7 @@ export const ActivityLibrary = (props: Props) => { { )} {templatesToRender.length === 0 ? (
-
+
No results found!
Try tapping a category above, using a different search, or creating exactly what @@ -340,49 +383,33 @@ export const ActivityLibrary = (props: Props) => {
) : ( <> - {categoryId === 'retrospective' && searchQuery.length === 0 ? ( + {sectionedTemplates ? ( <> - {(Object.keys(subCategoryMapping) as SubCategory[]).map( - (subCategory) => - subCategoryTemplates[subCategory].length > 0 && ( - <> -
- {subCategoryMapping[subCategory]} -
+ {Object.entries(sectionedTemplates).map( + ([subCategory, subCategoryTemplates]) => + subCategoryTemplates.length > 0 && ( + + {subCategory && ( +
+ {subCategory} +
+ )}
- +
) )} - {otherTemplates.length > 0 && ( - <> -
- Other activities -
-
- -
- - )} ) : ( <> - {quickStartTitle && ( -
- {quickStartTitle} -
- )}
diff --git a/packages/client/components/ActivityLibrary/Categories.ts b/packages/client/components/ActivityLibrary/Categories.ts index 4d67a35df6e..e50a16131e6 100644 --- a/packages/client/components/ActivityLibrary/Categories.ts +++ b/packages/client/components/ActivityLibrary/Categories.ts @@ -51,14 +51,14 @@ export const CATEGORY_THEMES: Record = { export const CATEGORY_ID_TO_NAME: Record = { [QUICK_START_CATEGORY_ID]: 'Quick Start', + [CUSTOM_CATEGORY_ID]: 'Custom', retrospective: 'Retrospective', estimation: 'Estimation', standup: 'Standup', feedback: 'Feedback', strategy: 'Strategy', premortem: 'Pre-Mortem', - postmortem: 'Post-Mortem', - [CUSTOM_CATEGORY_ID]: 'Custom' + postmortem: 'Post-Mortem' } export const MEETING_TYPE_TO_CATEGORY: Record = { diff --git a/packages/client/components/ActivityLibrary/CreateActivityCard.tsx b/packages/client/components/ActivityLibrary/CreateActivityCard.tsx index 0e08be225cf..ea574d4cf1c 100644 --- a/packages/client/components/ActivityLibrary/CreateActivityCard.tsx +++ b/packages/client/components/ActivityLibrary/CreateActivityCard.tsx @@ -19,9 +19,9 @@ const CreateActivityCard = (props: Props) => { Premium} + badge={Premium} > -
+
diff --git a/packages/client/components/ActivityLibrary/ScheduleMeetingButton.tsx b/packages/client/components/ActivityLibrary/ScheduleMeetingButton.tsx index a756aa9bccc..319187dd742 100644 --- a/packages/client/components/ActivityLibrary/ScheduleMeetingButton.tsx +++ b/packages/client/components/ActivityLibrary/ScheduleMeetingButton.tsx @@ -1,26 +1,35 @@ -import React, {useEffect, useState} from 'react' +import React from 'react' import graphql from 'babel-plugin-relay/macro' import SecondaryButton from '../SecondaryButton' -import GcalModal from '../../modules/userDashboard/components/GcalModal/GcalModal' -import {CreateGcalEventInput} from '../../__generated__/StartRetrospectiveMutation.graphql' -import GcalClientManager from '../../utils/GcalClientManager' -import useAtmosphere from '../../hooks/useAtmosphere' +import { + CreateGcalEventInput, + RecurrenceSettingsInput +} from '../../__generated__/StartRetrospectiveMutation.graphql' import {useFragment} from 'react-relay' import {ScheduleMeetingButton_team$key} from '~/__generated__/ScheduleMeetingButton_team.graphql' import {MenuMutationProps} from '../../hooks/useMutationProps' import useModal from '../../hooks/useModal' +import {ScheduleDialog} from '../ScheduleDialog' +import DialogContainer from '../DialogContainer' type Props = { mutationProps: MenuMutationProps - handleStartActivity: (gcalInput?: CreateGcalEventInput) => void + handleStartActivity: ( + gcalInput?: CreateGcalEventInput, + recurrenceInput?: RecurrenceSettingsInput + ) => void teamRef: ScheduleMeetingButton_team$key + placeholder: string + withRecurrence?: boolean } const ScheduleMeetingButton = (props: Props) => { - const {mutationProps, handleStartActivity, teamRef} = props - const atmosphere = useAtmosphere() - const [hasStartedGcalAuthTeamId, setHasStartedGcalAuthTeamId] = useState(null) - const {togglePortal: toggleModal, modalPortal} = useModal({ + const {mutationProps, handleStartActivity, teamRef, placeholder, withRecurrence} = props + const { + togglePortal: toggleModal, + closePortal: closeModal, + modalPortal + } = useModal({ id: 'createGcalEventModal' }) const {submitting} = mutationProps @@ -43,32 +52,26 @@ const ScheduleMeetingButton = (props: Props) => { } } } - ...GcalModal_team + ...ScheduleDialog_team } `, teamRef ) - const {id: teamId, viewerTeamMember} = team - const hasStartedGcalAuth = hasStartedGcalAuthTeamId === teamId + const {viewerTeamMember} = team const viewerGcalIntegration = viewerTeamMember?.integrations.gcal const cloudProvider = viewerGcalIntegration?.cloudProvider const handleClick = () => { - if (viewerGcalIntegration?.auth) { - toggleModal() - } else if (cloudProvider) { - const {clientId, id: providerId} = cloudProvider - GcalClientManager.openOAuth(atmosphere, providerId, clientId, teamId, mutationProps) - setHasStartedGcalAuthTeamId(teamId) - } + toggleModal() + } + const onStartActivity = ( + gcalInput?: CreateGcalEventInput, + recurrenceInput?: RecurrenceSettingsInput + ) => { + handleStartActivity(gcalInput, recurrenceInput) + closeModal() } - - useEffect(() => { - if (hasStartedGcalAuth && viewerGcalIntegration?.auth) { - toggleModal() - } - }, [hasStartedGcalAuth, viewerGcalIntegration]) if (!cloudProvider) return null return ( @@ -77,11 +80,16 @@ const ScheduleMeetingButton = (props: Props) => {
Schedule
{modalPortal( - + + + )} ) diff --git a/packages/client/components/AddActivityButton.tsx b/packages/client/components/AddActivityButton.tsx deleted file mode 100644 index 53899b38267..00000000000 --- a/packages/client/components/AddActivityButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import styled from '@emotion/styled' -import {Add} from '@mui/icons-material' -import React from 'react' -import {PALETTE} from '~/styles/paletteV3' -import PlainButton from './PlainButton/PlainButton' - -const StyledPlainButton = styled(PlainButton)({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - color: PALETTE.SKY_500, - fontWeight: 600, - fontSize: 14, - margin: '0 8px', - ':hover, :focus, :active': { - color: PALETTE.SKY_600 - }, - transition: 'color 0.1s ease' -}) - -const Icon = styled(Add)({ - width: 20, - height: 20, - margin: '0 4px 0 0' -}) - -interface Props { - onClick: () => void - disabled?: boolean -} - -const AddActivityButton = (props: Props) => { - const {onClick, disabled} = props - - return ( - - -
Add an activity
-
- ) -} - -export default AddActivityButton diff --git a/packages/client/components/AuthenticationDialog.tsx b/packages/client/components/AuthenticationDialog.tsx index 3ed5ad9f103..a53e250c3e1 100644 --- a/packages/client/components/AuthenticationDialog.tsx +++ b/packages/client/components/AuthenticationDialog.tsx @@ -1,11 +1,12 @@ import styled from '@emotion/styled' import InviteDialog from './InviteDialog' +export const AUTH_DIALOG_WIDTH = 356 const AuthenticationDialog = styled(InviteDialog)({ alignItems: 'center', paddingTop: 24, paddingBottom: 24, - width: 356 + width: AUTH_DIALOG_WIDTH }) export default AuthenticationDialog diff --git a/packages/client/components/AuthenticationPage.tsx b/packages/client/components/AuthenticationPage.tsx index dd3f6808e82..c89eb2c9be2 100644 --- a/packages/client/components/AuthenticationPage.tsx +++ b/packages/client/components/AuthenticationPage.tsx @@ -5,13 +5,15 @@ import useAtmosphere from '../hooks/useAtmosphere' import useDocumentTitle from '../hooks/useDocumentTitle' import useRouter from '../hooks/useRouter' import getValidRedirectParam from '../utils/getValidRedirectParam' +import {AUTH_DIALOG_WIDTH} from './AuthenticationDialog' import GenericAuthentication, {AuthPageSlug, GotoAuthPage} from './GenericAuthentication' import TeamInvitationWrapper from './TeamInvitationWrapper' const CopyBlock = styled('div')({ marginBottom: 48, width: 'calc(100vw - 16px)', - maxWidth: 500, + // must be no wider than the auth popup width to keep it looking clean + maxWidth: AUTH_DIALOG_WIDTH, textAlign: 'center' }) diff --git a/packages/client/components/DeleteTeamDialog.tsx b/packages/client/components/DeleteTeamDialog.tsx new file mode 100644 index 00000000000..d6bb993bbde --- /dev/null +++ b/packages/client/components/DeleteTeamDialog.tsx @@ -0,0 +1,74 @@ +import React, {useState} from 'react' +import FlatPrimaryButton from './FlatPrimaryButton' +import {Input} from '../ui/Input/Input' +import {Dialog} from '../ui/Dialog/Dialog' +import {DialogContent} from '../ui/Dialog/DialogContent' +import {DialogTitle} from '../ui/Dialog/DialogTitle' +import {DialogActions} from '../ui/Dialog/DialogActions' +import useMutationProps from '../hooks/useMutationProps' +import SecondaryButton from './SecondaryButton' +import ArchiveTeamMutation from '../mutations/ArchiveTeamMutation' +import useAtmosphere from '../hooks/useAtmosphere' +import useRouter from '../hooks/useRouter' + +interface Props { + isOpen: boolean + onClose: () => void + onDeleteTeam: (teamId: string) => void + teamId: string + teamName: string + teamOrgId: string +} + +const DeleteTeamDialog = (props: Props) => { + const atmosphere = useAtmosphere() + const {history} = useRouter() + const {isOpen, onClose, teamId, teamName, teamOrgId, onDeleteTeam} = props + + const {submitting, onCompleted, onError, error, submitMutation} = useMutationProps() + + const [typedTeamName, setTypedTeamName] = useState(false) + + const handleDeleteTeam = () => { + if (submitting) return + submitMutation() + ArchiveTeamMutation(atmosphere, {teamId}, {history, onError, onCompleted}) + onDeleteTeam(teamId) + history.push(`/me/organizations/${teamOrgId}/teams`) + } + + return ( + + + Delete Team + +
+ + { + e.preventDefault() + if (e.target.value === teamName) setTypedTeamName(true) + else setTypedTeamName(false) + }} + placeholder={teamName} + /> + {error && ( +
{error.message}
+ )} +
+ + + + I understand the consequences, delete this team + + Cancel + +
+
+ ) +} + +export default DeleteTeamDialog diff --git a/packages/client/components/DiscussionThreadInput.tsx b/packages/client/components/DiscussionThreadInput.tsx index 06f8a1a2fd5..ba7cd2f3778 100644 --- a/packages/client/components/DiscussionThreadInput.tsx +++ b/packages/client/components/DiscussionThreadInput.tsx @@ -29,8 +29,6 @@ import {createLocalPoll} from './Poll/local/newPoll' import SendCommentButton from './SendCommentButton' import CommentEditor from './TaskEditor/CommentEditor' import {ReplyMention, SetReplyMention} from './ThreadedItem' -import AddActivityButton from '~/components/AddActivityButton' -import SendClientSideEvent from '../utils/SendClientSideEvent' const Wrapper = styled('div')<{isReply: boolean; isDisabled: boolean}>(({isDisabled, isReply}) => ({ display: 'flex', @@ -106,9 +104,6 @@ const DiscussionThreadInput = forwardRef((props: Props, ref: any) => { graphql` fragment DiscussionThreadInput_viewer on User { picture - featureFlags { - retrosInDisguise - } } `, viewerRef @@ -127,7 +122,7 @@ const DiscussionThreadInput = forwardRef((props: Props, ref: any) => { `, discussionRef ) - const {picture, featureFlags} = viewer + const {picture} = viewer const isReply = !!props.isReply const isDisabled = !!props.isDisabled const {id: discussionId, meetingId, isAnonymousComment, team, discussionTopicType} = discussion @@ -298,8 +293,7 @@ const DiscussionThreadInput = forwardRef((props: Props, ref: any) => { } }, []) - const allowAddActivity = featureFlags.retrosInDisguise - const isActionsContainerVisible = allowTasks || allowPolls || allowAddActivity + const isActionsContainerVisible = allowTasks || allowPolls const isActionsContainerDisabled = isCreatingTask || isCreatingPoll const avatar = isAnonymousComment ? anonymousAvatar : picture @@ -345,15 +339,6 @@ const DiscussionThreadInput = forwardRef((props: Props, ref: any) => { disabled={isActionsContainerDisabled} /> )} - {allowAddActivity && ( - { - window.open(`/activity-library/category/recommended`, '_blank', 'noreferrer') - SendClientSideEvent(atmosphere, 'Add Activity Button Clicked') - }} - disabled={isActionsContainerDisabled} - /> - )} )} diff --git a/packages/client/components/EmailPasswordAuthForm.tsx b/packages/client/components/EmailPasswordAuthForm.tsx index ed195b49b16..d124dd8428c 100644 --- a/packages/client/components/EmailPasswordAuthForm.tsx +++ b/packages/client/components/EmailPasswordAuthForm.tsx @@ -30,6 +30,8 @@ import RaisedButton from './RaisedButton' import StyledTip from './StyledTip' interface Props { + // used to determine the coordinates of the auth popup + getOffsetTop?: () => number email: string invitationToken: string | undefined | null // is the primary login action (not secondary to Google Oauth) @@ -38,9 +40,6 @@ interface Props { goToPage?: (page: AuthPageSlug, params: string) => void } -const FieldGroup = styled('div')({ - margin: '16px 0' -}) const FieldBlock = styled('div')<{isSSO?: boolean}>(({isSSO}) => ({ margin: '0 0 1.25rem', visibility: isSSO ? 'hidden' : undefined @@ -90,7 +89,7 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => { const isInternalAuthEnabled = window.__ACTION__.AUTH_INTERNAL_ENABLED const isSSOAuthEnabled = window.__ACTION__.AUTH_SSO_ENABLED - const {isPrimary, isSignin, invitationToken, email, goToPage} = props + const {getOffsetTop, isPrimary, isSignin, invitationToken, email, goToPage} = props const {location} = useRouter() const params = new URLSearchParams(location.search) const isSSODefault = isSSOAuthEnabled && Boolean(params.get('sso')) @@ -105,7 +104,7 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => { const {fields, onChange, setDirtyField, validateField} = useForm({ email: { getDefault: () => email, - validate: validateEmail + validate: signInWithSSOOnly ? undefined : validateEmail }, password: { getDefault: () => '', @@ -150,6 +149,7 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => { const domain = getSSODomainFromEmail(email)! const validSSOURL = domain === ssoDomain && ssoURL const isProbablySSO = isSSO || !fields.password.value || validSSOURL + const top = getOffsetTop?.() || 56 let optimisticPopup if (isProbablySSO) { // Safari blocks all calls to window.open that are not triggered SYNCHRONOUSLY from an event @@ -164,7 +164,7 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => { optimisticPopup = window.open( '', 'SSO', - getOAuthPopupFeatures({width: 385, height: 550, top: 64}) + getOAuthPopupFeatures({width: 385, height: 576, top}) ) } const url = validSSOURL || (await getSSOUrl(atmosphere, email)) @@ -173,7 +173,7 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => { return false } submitMutation() - const response = await getTokenFromSSO(url) + const response = await getTokenFromSSO(url, top) if ('error' in response) { onError(new Error(response.error || 'Error logging in')) return true @@ -198,6 +198,7 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => { const onSubmit = async (e: React.FormEvent) => { e.preventDefault() if (submitting) return + onCompleted() setDirtyField() const {email: emailRes, password: passwordRes} = validateField() if (emailRes.error) return @@ -244,8 +245,8 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => {
{error && } {isSSO && submitting && Continue through the login popup} - - +
+ { /> )} - +
+

{team.name}

+
+
+ +
+
+
+
{teamMembers.length} Active
+
+
+ {teamMembers.map((teamMember) => ( + + ))} +
+ + {menuPortal( + + )} + + {isDeleteTeamDialogOpened ? ( + + ) : null} +
+ ) +} diff --git a/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersMenu.tsx b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersMenu.tsx new file mode 100644 index 00000000000..bc53031bfc8 --- /dev/null +++ b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersMenu.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import {MenuProps} from '../../../../hooks/useMenu' +import Menu from '../../../../components/Menu' +import MenuItem from '../../../../components/MenuItem' + +interface OrgTeamMembersMenuProps { + menuProps: MenuProps + openDeleteTeamModal: () => void +} + +export const OrgTeamMembersMenu = (props: OrgTeamMembersMenuProps) => { + const {menuProps, openDeleteTeamModal} = props + const {closePortal} = menuProps + + return ( + + { + closePortal() + openDeleteTeamModal() + }} + /> + + ) +} diff --git a/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRoot.tsx b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRoot.tsx new file mode 100644 index 00000000000..c0c959d084c --- /dev/null +++ b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRoot.tsx @@ -0,0 +1,25 @@ +import React, {Suspense} from 'react' +import orgTeamMembersQuery, {OrgTeamMembersQuery} from '~/__generated__/OrgTeamMembersQuery.graphql' +import useQueryLoaderNow from '../../../../hooks/useQueryLoaderNow' +import {LoaderSize} from '../../../../types/constEnums' +import {Loader} from '../../../../utils/relay/renderLoader' +import {OrgTeamMembers} from './OrgTeamMembers' +import useRouter from '../../../../hooks/useRouter' + +const OrgTeamMembersRoot = () => { + const {match} = useRouter<{teamId: string}>() + const { + params: {teamId} + } = match + const queryRef = useQueryLoaderNow(orgTeamMembersQuery, { + teamId + }) + + return ( + }> + {queryRef && } + + ) +} + +export default OrgTeamMembersRoot diff --git a/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRow.tsx b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRow.tsx new file mode 100644 index 00000000000..02b643f744f --- /dev/null +++ b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRow.tsx @@ -0,0 +1,117 @@ +import React from 'react' +import graphql from 'babel-plugin-relay/macro' +import {useFragment} from 'react-relay' +import {OrgTeamMembersRow_teamMember$key} from '../../../../__generated__/OrgTeamMembersRow_teamMember.graphql' +import {Avatar} from '../../../../ui/Avatar/Avatar' +import {AvatarFallback} from '../../../../ui/Avatar/AvatarFallback' +import {AvatarImage} from '../../../../ui/Avatar/AvatarImage' +import {Button} from '../../../../ui/Button/Button' +import {MoreVert} from '@mui/icons-material' +import {OrgTeamMemberMenu} from './OrgTeamMemberMenu' +import {MenuPosition} from '../../../../hooks/useCoords' +import useMenu from '../../../../hooks/useMenu' +import PromoteTeamMemberModal from '../../../teamDashboard/components/PromoteTeamMemberModal/PromoteTeamMemberModal' +import RemoveTeamMemberModal from '../../../teamDashboard/components/RemoveTeamMemberModal/RemoveTeamMemberModal' +import useModal from '../../../../hooks/useModal' +import useAtmosphere from '../../../../hooks/useAtmosphere' + +type Props = { + teamMemberRef: OrgTeamMembersRow_teamMember$key + isViewerLead: boolean + isViewerOrgAdmin: boolean +} + +export const OrgTeamMembersRow = (props: Props) => { + const {teamMemberRef} = props + const teamMember = useFragment( + graphql` + fragment OrgTeamMembersRow_teamMember on TeamMember { + ...OrgTeamMemberMenu_teamMember + userId + picture + preferredName + isLead + isOrgAdmin + isSelf + email + ...PromoteTeamMemberModal_teamMember + ...RemoveTeamMemberModal_teamMember + } + `, + teamMemberRef + ) + + const {isViewerLead, isViewerOrgAdmin} = props + const {isLead, isOrgAdmin, userId} = teamMember + + const atmosphere = useAtmosphere() + const {viewerId} = atmosphere + const isSelf = userId === viewerId + + const showMenuButton = (isViewerOrgAdmin && !isLead) || (isViewerLead && !isSelf && !isOrgAdmin) + + const {togglePortal, originRef, menuPortal, menuProps} = useMenu(MenuPosition.UPPER_RIGHT) + const { + closePortal: closePromote, + togglePortal: togglePromote, + modalPortal: portalPromote + } = useModal() + const { + closePortal: closeRemove, + togglePortal: toggleRemove, + modalPortal: portalRemove + } = useModal() + + return ( +
+
+ + + {teamMember.preferredName.substring(0, 2)} + +
+
+
+ {teamMember.preferredName}{' '} + {teamMember.isLead ? ( + + Team Lead + + ) : null} +
+ +
+
+ {showMenuButton && ( + + )} + {menuPortal( + + )} +
+ + {portalPromote()} + {portalRemove()} +
+ ) +} diff --git a/packages/client/modules/userDashboard/components/OrgTeams/OrgTeams.tsx b/packages/client/modules/userDashboard/components/OrgTeams/OrgTeams.tsx index e9bd39f71d7..3f544d7dd27 100644 --- a/packages/client/modules/userDashboard/components/OrgTeams/OrgTeams.tsx +++ b/packages/client/modules/userDashboard/components/OrgTeams/OrgTeams.tsx @@ -1,16 +1,12 @@ import React from 'react' import graphql from 'babel-plugin-relay/macro' -import styled from '@emotion/styled' -import Row from '../../../../components/Row/Row' -import Panel from '../../../../components/Panel/Panel' -import {ElementWidth} from '../../../../types/constEnums' import {useFragment} from 'react-relay' import OrgTeamsRow from './OrgTeamsRow' import {OrgTeams_organization$key} from '../../../../__generated__/OrgTeams_organization.graphql' - -const StyledPanel = styled(Panel)({ - maxWidth: ElementWidth.PANEL_WIDTH -}) +import AddTeamDialogRoot from '../../../../components/AddTeamDialogRoot' +import {Button} from '../../../../ui/Button/Button' +import {useDialogState} from '../../../../ui/Dialog/useDialogState' +import plural from '../../../../utils/plural' type Props = { organizationRef: OrgTeams_organization$key @@ -31,23 +27,48 @@ const OrgTeams = (props: Props) => { `, organizationRef ) + const { + open: openAddTeamDialog, + close: closeAddTeamDialog, + isOpen: isAddTeamDialogOpened + } = useDialogState() + const {allTeams, isBillingLeader} = organization if (!isBillingLeader) return null + return ( - <> -

{'Teams'}

- - -
-
Team Name
-
Lead
+
+
+

Teams

+
+ +
+
+ +
+
+
+
+ {allTeams.length} {plural(allTeams.length, 'Team')} +
- +
{allTeams.map((team) => ( ))} - - +
+ + {isAddTeamDialogOpened ? ( + + ) : null} +
) } diff --git a/packages/client/modules/userDashboard/components/OrgTeams/OrgTeamsRow.tsx b/packages/client/modules/userDashboard/components/OrgTeams/OrgTeamsRow.tsx index 00123a43b5d..d6da2e5e6c6 100644 --- a/packages/client/modules/userDashboard/components/OrgTeams/OrgTeamsRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgTeams/OrgTeamsRow.tsx @@ -1,8 +1,9 @@ import React from 'react' import {Link} from 'react-router-dom' import graphql from 'babel-plugin-relay/macro' -import Row from '../../../../components/Row/Row' import {useFragment} from 'react-relay' +import {ChevronRight} from '@mui/icons-material' + import plural from '../../../../utils/plural' import {OrgTeamsRow_team$key} from '../../../../__generated__/OrgTeamsRow_team.graphql' @@ -30,42 +31,26 @@ const OrgTeamsRow = (props: Props) => { ) const {id: teamId, teamMembers, name} = team const teamMembersCount = teamMembers.length - const teamLeadEmail = teamMembers.find((member) => member.isLead)?.email ?? '' - const isViewerTeamLead = teamMembers.some( - (member) => member.isSelf && (member.isLead || member.isOrgAdmin) - ) + return ( - -
-
{name}
-
-
- {`${teamMembersCount} ${plural(teamMembersCount, 'member')}`} - {isViewerTeamLead && ( - <> - - - {'Manage Team'} - - - )} + +
+
+
{name}
+
+
+ {`${teamMembersCount} ${plural(teamMembersCount, 'member')}`} +
- - - {`${teamLeadEmail} ${isViewerTeamLead ? '(You)' : ''}`} - +
+
+
- + ) } diff --git a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx index eda83c7b6dc..8a15e046096 100644 --- a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx @@ -18,13 +18,18 @@ import RoleTag from '../../../../components/Tag/RoleTag' import {MenuPosition} from '../../../../hooks/useCoords' import useMenu from '../../../../hooks/useMenu' import useModal from '../../../../hooks/useModal' -import useTooltip from '../../../../hooks/useTooltip' import defaultUserAvatar from '../../../../styles/theme/images/avatar-user.svg' import {Breakpoint} from '../../../../types/constEnums' import lazyPreload from '../../../../utils/lazyPreload' import withMutationProps, {WithMutationProps} from '../../../../utils/relay/withMutationProps' -import {OrgMemberRow_organization$key} from '../../../../__generated__/OrgMemberRow_organization.graphql' -import {OrgMemberRow_organizationUser$key} from '../../../../__generated__/OrgMemberRow_organizationUser.graphql' +import { + OrgMemberRow_organization$key, + OrgMemberRow_organization$data +} from '../../../../__generated__/OrgMemberRow_organization.graphql' +import { + OrgMemberRow_organizationUser$key, + OrgMemberRow_organizationUser$data +} from '../../../../__generated__/OrgMemberRow_organizationUser.graphql' import BaseTag from '../../../../components/Tag/BaseTag' const AvatarBlock = styled('div')({ @@ -59,6 +64,7 @@ const MenuToggleBlock = styled('div')({ interface Props extends WithMutationProps { billingLeaderCount: number + orgAdminCount: number organizationUser: OrgMemberRow_organizationUser$key organization: OrgMemberRow_organization$key } @@ -90,29 +96,157 @@ const BillingLeaderActionMenu = lazyPreload( /* webpackChunkName: 'BillingLeaderActionMenu' */ '../../../../components/BillingLeaderActionMenu' ) ) +const OrgAdminActionMenu = lazyPreload( + () => + import(/* webpackChunkName: 'OrgAdminActionMenu' */ '../../../../components/OrgAdminActionMenu') +) const RemoveFromOrgModal = lazyPreload( () => import(/* webpackChunkName: 'RemoveFromOrgModal' */ '../RemoveFromOrgModal/RemoveFromOrgModal') ) +interface UserAvatarProps { + picture?: string +} + +const UserAvatar: React.FC = ({picture}) => ( + + {picture ? ( + + ) : ( + default avatar + )} + +) + +interface UserInfoProps { + preferredName: string + email: string + isBillingLeader: boolean + isOrgAdmin: boolean + inactive: boolean | null + newUserUntil: string +} + +const UserInfo: React.FC = ({ + preferredName, + email, + isBillingLeader, + isOrgAdmin, + inactive, + newUserUntil +}) => ( + + + {preferredName} + {isBillingLeader && Billing Leader} + {isOrgAdmin && Org Admin} + {inactive && !isBillingLeader && !isOrgAdmin && Inactive} + {new Date(newUserUntil) > new Date() && New} + + + {email} + + +) + +interface UserActionsProps { + isViewerOrgAdmin: boolean + isViewerBillingLeader: boolean + isViewerLastOrgAdmin: boolean + isViewerLastBillingLeader: boolean + organization: OrgMemberRow_organization$data + organizationUser: OrgMemberRow_organizationUser$data + preferredName: string + viewerId: string +} + +const UserActions: React.FC = ({ + isViewerOrgAdmin, + isViewerBillingLeader, + isViewerLastOrgAdmin, + isViewerLastBillingLeader, + organizationUser, + organization, + preferredName, + viewerId +}) => { + const {orgId} = organization + const { + user: {userId} + } = organizationUser + const {togglePortal, originRef, menuPortal, menuProps} = useMenu(MenuPosition.UPPER_RIGHT) + const {togglePortal: toggleLeave, modalPortal: leaveModal} = useModal() + const {togglePortal: toggleRemove, modalPortal: removeModal} = useModal() + const actionMenuProps = { + menuProps, + originRef, + togglePortal, + toggleLeave, + toggleRemove, + isViewerLastOrgAdmin, + isViewerLastBillingLeader, + organization, + organizationUser + } + + const showLeaveButton = !isViewerOrgAdmin && !isViewerBillingLeader && viewerId === userId + + return ( + + + {showLeaveButton && ( + + Leave Organization + + )} + {(isViewerOrgAdmin || (isViewerBillingLeader && !isViewerLastBillingLeader)) && ( + + + + )} + {isViewerOrgAdmin && menuPortal()} + {!isViewerOrgAdmin && + isViewerBillingLeader && + menuPortal()} + {leaveModal()} + {removeModal( + + )} + + + ) +} + const OrgMemberRow = (props: Props) => { const atmosphere = useAtmosphere() const { billingLeaderCount, + orgAdminCount, organizationUser: organizationUserRef, organization: organizationRef } = props + const organization = useFragment( graphql` fragment OrgMemberRow_organization on Organization { isViewerBillingLeader: isBillingLeader + isViewerOrgAdmin: isOrgAdmin orgId: id ...BillingLeaderActionMenu_organization + ...OrgAdminActionMenu_organization } `, organizationRef ) + const organizationUser = useFragment( graphql` fragment OrgMemberRow_organizationUser on OrganizationUser { @@ -126,103 +260,49 @@ const OrgMemberRow = (props: Props) => { role newUserUntil ...BillingLeaderActionMenu_organizationUser + ...OrgAdminActionMenu_organizationUser } `, organizationUserRef ) - const {orgId, isViewerBillingLeader} = organization - const {newUserUntil, user, role} = organizationUser + + const {isViewerBillingLeader, isViewerOrgAdmin} = organization + + const { + newUserUntil, + user: {email, inactive, picture, preferredName}, + role + } = organizationUser + + const {viewerId} = atmosphere + const isBillingLeader = role === 'BILLING_LEADER' const isOrgAdmin = role === 'ORG_ADMIN' - const {email, inactive, picture, preferredName, userId} = user const isViewerLastBillingLeader = isViewerBillingLeader && isBillingLeader && billingLeaderCount === 1 - const {viewerId} = atmosphere - const {togglePortal, originRef, menuPortal, menuProps} = useMenu(MenuPosition.UPPER_RIGHT) - const {togglePortal: toggleLeave, modalPortal: leaveModal} = useModal() - const {togglePortal: toggleRemove, modalPortal: removeModal} = useModal() - const { - tooltipPortal, - openTooltip, - closeTooltip, - originRef: tooltipRef - } = useTooltip(MenuPosition.LOWER_RIGHT) - const canViewMenu = !isViewerLastBillingLeader && organizationUser.role !== 'ORG_ADMIN' + const isViewerLastOrgAdmin = isViewerOrgAdmin && isOrgAdmin && orgAdminCount === 1 return ( - - {picture ? ( - - ) : ( - - )} - - - - {preferredName} - {isBillingLeader && {'Billing Leader'}} - {isOrgAdmin && {'Org Admin'}} - {inactive && !isBillingLeader && !isOrgAdmin && {'Inactive'}} - {new Date(newUserUntil) > new Date() && {'New'}} - - - {email} - - - - - {!isBillingLeader && !isOrgAdmin && viewerId === userId && ( - - Leave Organization - - )} - {!canViewMenu && ( - - {tooltipPortal( - isViewerLastBillingLeader ? ( -
- {'You need to promote another Billing Leader'} -
- {'before you can remove this role.'} -
- ) : ( -
Contact support (love@parabol.co) to remove the Org Admin role
- ) - )} - -
- )} - {isViewerBillingLeader && canViewMenu && ( - - - - )} - {menuPortal( - - )} - {leaveModal()} - {removeModal( - - )} -
-
+ + +
) } diff --git a/packages/client/modules/userDashboard/containers/OrgMembers/OrgMembersRoot.tsx b/packages/client/modules/userDashboard/containers/OrgMembers/OrgMembersRoot.tsx index b0a7ab43373..8242ebf2b8e 100644 --- a/packages/client/modules/userDashboard/containers/OrgMembers/OrgMembersRoot.tsx +++ b/packages/client/modules/userDashboard/containers/OrgMembers/OrgMembersRoot.tsx @@ -15,6 +15,7 @@ const OrgMembersRoot = (props: Props) => { orgId, first: 10000 }) + return ( }> {queryRef && } diff --git a/packages/client/mutations/ArchiveTeamMutation.ts b/packages/client/mutations/ArchiveTeamMutation.ts index 15afd828d4b..218fdd0c7c1 100644 --- a/packages/client/mutations/ArchiveTeamMutation.ts +++ b/packages/client/mutations/ArchiveTeamMutation.ts @@ -30,6 +30,14 @@ graphql` activeMeetings { id } + organization { + allTeams { + id + } + viewerTeams { + id + } + } } teamTemplateIds } diff --git a/packages/client/package.json b/packages/client/package.json index 085bd29d0b3..82271ce03b8 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.20.0", + "version": "7.22.2", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -75,13 +75,16 @@ "@mui/icons-material": "^5.8.4", "@mui/material": "^5.9.2", "@mui/x-date-pickers": "^6.3.1", + "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.4", "@radix-ui/react-radio-group": "^1.1.2", "@radix-ui/react-scroll-area": "^1.0.3", "@radix-ui/react-select": "^1.2.2", - "@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-alert-dialog": "1.0.5", + "@radix-ui/react-tooltip": "^1.0.7", "@sentry/browser": "^5.8.0", "@stripe/react-stripe-js": "^1.16.5", "@stripe/stripe-js": "^1.47.0", diff --git a/packages/client/serviceWorker/sw.ts b/packages/client/serviceWorker/sw.ts index 3fba470695b..e35d7f86db7 100644 --- a/packages/client/serviceWorker/sw.ts +++ b/packages/client/serviceWorker/sw.ts @@ -18,7 +18,7 @@ const DYNAMIC_CACHE = `parabol-dynamic-${__APP_VERSION__}` const cacheList = [STATIC_CACHE, DYNAMIC_CACHE] // this gets built in applyEnvVarToClientAssets -const PUBLIC_PATH = `__PUBLIC_PATH__`.replace(/\/{2,}/, 'https://') +const PUBLIC_PATH = `__PUBLIC_PATH__`.replace(/^\/{2,}/, 'https://') const waitUntil = (cb: (e: ExtendableEvent) => void) => (e: ExtendableEvent) => { e.waitUntil(cb(e)) } diff --git a/packages/client/styles/theme/images/anonymous-avatar.png b/packages/client/styles/theme/images/anonymous-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..791dcd86512fdd5030d2c560ef7fa6dd1238a5a2 GIT binary patch literal 1843 zcmV-32h8}1P);M(*l7D2&O8eB&ZVo_Y@!P>i;Jtj)HU+Hrt71+X2^z41hdg_0|4!IHyD<#@dBIWIqm_^ zvDYo^T#H7drqhXBP5|TaFW)Yzm5E5>rc>DIf z3)|bfpKL4YWTRoZoPZsb+f6l3OM!$x;Qvh|GP?1N0dFoOK795p`&&X9%r^l$Du!Xm z*|XAL=jNyW&=p`j9_I^rb19q2PkftTMw!!>*owN(5X&Q35oLzV1 zaye+)iMPnf{)+m;<>j@BeZW$&__31=kxr-KBtU^8Ct4;Z>FYJzE+*mkgQ0YIzjS7=SEk2wkI^?Gfe$fNRlJdXS|T_2-0O_!Vm38Dx= z6dg^}t#pmZv{5(sZa>g$HenbB48wp)Of;+p0KiST!7wx!rUxv?b`4ms6M?p@Cf&)g z*=(Yysz_(Ds8n`Qt=3SfR#4XsG@I==K1I2}vK&NFgeZ#edOZk*Lh$?jaJkywY^G_t z+rp`Z0-ij5ic~6vTFw4Ndr+3ugr;fGG!6ND9suy*!G9p|0tU|XWAN-*1p4}38c$P{ z%WO1oEXCDoH4xK8e}6yv9pqA{BBqI2tp-65j>FM(gYIE_G(xmvfPx^L7^LYsGU*Hq z!+^)@K{ylwMIEc!005R{A(PHvw^Bh5%OD&IgX8S?S}e=dXrAYkYDGI`Ct!2)G1k`Z zf~MUNL;<@yyI|QK{AhL-exL7BD=RA)A0Nla$eREFViK&b+`;0_7=H5ZPr>%|008RsI%129kYyQ@ zQxjn5y#;#o=qs!wRv`!?WU2G*CJ4fPn&;T-0Dj)ddHvx-3=a+M!vO$8Gnk&9!e^h| zz&~%?K)t?K6S6D=mSr(LJq3z#@B5F8yopk&g!T1xdw?SMDP>{j8pp96OJG%15e|os z*(u76P$&eJumUC`** zD9WWWmX?=MDwRP9fubl_jKv@d0?rM;Xat9bh9QUo7Gp6eiUL9iO3!1<E*|)z04m3@-Zap0Q`m4?Vwd+et#S#*$D=4Zj_RyI#XD~52v44VFsv8FGtgK=y zncTMp0)9+QPQm9td?g(e!!YE+$k2Y%bf|H^xv=u*jg92*+x~k1D%C3LnhuZ0)3F$+ z*XyY4R>1NcJT|JJ;qMImX?}j{kFCFty)69W?@PC{nfwnre(4n`6b{DUeeZJQpzYXk zdhchKXM;ii*RKz2Pr*LlFaZy-!{})}Dev-(x8(O`bo0zJ7G9 z?em*jFmGnk`RP{)7YYaCBNsz+C&9G^Y-wR3@j+^9`*%)W9qg!wWsLp->BEBuqANgP z@7-oQSN=rFs?(Nbbxs3qL{XF_$$>BYf#9zrk++_9@^|!zzPx<*d%HW@$Ax_9a#xS& zZ}|S@_3~F+m#`f<0&FQ3i_va(?=Q5fenr#uF +>(({className, ...props}, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName diff --git a/packages/client/ui/AlertDialog/AlertDialogCancel.tsx b/packages/client/ui/AlertDialog/AlertDialogCancel.tsx new file mode 100644 index 00000000000..201ca648679 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogCancel.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import clsx from 'clsx' + +const AlertDialogCancel = React.forwardRef< + HTMLButtonElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName diff --git a/packages/client/ui/AlertDialog/AlertDialogContent.tsx b/packages/client/ui/AlertDialog/AlertDialogContent.tsx new file mode 100644 index 00000000000..dbcd0de0582 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogContent.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import clsx from 'clsx' +import {AlertDialogOverlay} from './AlertDialogOverlay' +import {AlertDialogPortal} from './AlertDialog' + +const AlertDialogContent = React.forwardRef< + HTMLDivElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName diff --git a/packages/client/ui/AlertDialog/AlertDialogDescription.tsx b/packages/client/ui/AlertDialog/AlertDialogDescription.tsx new file mode 100644 index 00000000000..7c940b02fec --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogDescription.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import clsx from 'clsx' + +const AlertDialogDescription = React.forwardRef< + HTMLParagraphElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName diff --git a/packages/client/ui/AlertDialog/AlertDialogFooter.tsx b/packages/client/ui/AlertDialog/AlertDialogFooter.tsx new file mode 100644 index 00000000000..0148d9a8c39 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogFooter.tsx @@ -0,0 +1,10 @@ +import * as React from 'react' +import clsx from 'clsx' + +const AlertDialogFooter = ({className, ...props}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = 'AlertDialogFooter' diff --git a/packages/client/ui/AlertDialog/AlertDialogHeader.tsx b/packages/client/ui/AlertDialog/AlertDialogHeader.tsx new file mode 100644 index 00000000000..869b09ccfc3 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogHeader.tsx @@ -0,0 +1,7 @@ +import * as React from 'react' +import clsx from 'clsx' + +const AlertDialogHeader = ({className, ...props}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = 'AlertDialogHeader' diff --git a/packages/client/ui/AlertDialog/AlertDialogOverlay.tsx b/packages/client/ui/AlertDialog/AlertDialogOverlay.tsx new file mode 100644 index 00000000000..e867c7ed363 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogOverlay.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import clsx from 'clsx' + +export const AlertDialogOverlay = React.forwardRef< + HTMLDivElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName diff --git a/packages/client/ui/AlertDialog/AlertDialogPortal.tsx b/packages/client/ui/AlertDialog/AlertDialogPortal.tsx new file mode 100644 index 00000000000..c4e8d56916f --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogPortal.tsx @@ -0,0 +1,3 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +const AlertDialogPortal = AlertDialogPrimitive.Portal diff --git a/packages/client/ui/AlertDialog/AlertDialogTitle.tsx b/packages/client/ui/AlertDialog/AlertDialogTitle.tsx new file mode 100644 index 00000000000..c7c14e011c5 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogTitle.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import clsx from 'clsx' + +const AlertDialogTitle = React.forwardRef< + HTMLHeadingElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName diff --git a/packages/client/ui/AlertDialog/AlertDialogTrigger.tsx b/packages/client/ui/AlertDialog/AlertDialogTrigger.tsx new file mode 100644 index 00000000000..0a20feddfc1 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogTrigger.tsx @@ -0,0 +1,3 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger diff --git a/packages/client/ui/Avatar/Avatar.tsx b/packages/client/ui/Avatar/Avatar.tsx new file mode 100644 index 00000000000..23bbecd22a6 --- /dev/null +++ b/packages/client/ui/Avatar/Avatar.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' +import clsx from 'clsx' + +export const Avatar = React.forwardRef< + HTMLSpanElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) + +Avatar.displayName = AvatarPrimitive.Root.displayName diff --git a/packages/client/ui/Avatar/AvatarFallback.tsx b/packages/client/ui/Avatar/AvatarFallback.tsx new file mode 100644 index 00000000000..482648102a8 --- /dev/null +++ b/packages/client/ui/Avatar/AvatarFallback.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' +import clsx from 'clsx' + +export const AvatarFallback = React.forwardRef< + HTMLSpanElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) + +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName diff --git a/packages/client/ui/Avatar/AvatarImage.tsx b/packages/client/ui/Avatar/AvatarImage.tsx new file mode 100644 index 00000000000..66f41abad5d --- /dev/null +++ b/packages/client/ui/Avatar/AvatarImage.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' +import clsx from 'clsx' + +export const AvatarImage = React.forwardRef< + HTMLImageElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) + +AvatarImage.displayName = AvatarPrimitive.Image.displayName diff --git a/packages/client/ui/Button/Button.tsx b/packages/client/ui/Button/Button.tsx index a60c67700d7..90554dc662e 100644 --- a/packages/client/ui/Button/Button.tsx +++ b/packages/client/ui/Button/Button.tsx @@ -7,20 +7,21 @@ type Size = 'sm' | 'md' | 'lg' | 'default' type Shape = 'pill' | 'circle' | 'default' const BASE_STYLES = - 'cursor-pointer inline-flex items-center justify-center whitespace-nowrap font-semibold transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50' + 'cursor-pointer inline-flex items-center justify-center whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50' // TODO: make sure the styles match the designs const VARIANT_STYLES: Record = { - primary: 'bg-primary text-white hover:bg-primary/90', - destructive: 'bg-tomato-500 text-white hover:bg-tomato-500/90', - outline: 'text-slate-900 border border-slate-400 hover:bg-slate-200 px-2.5 py-1 bg-transparent', - secondary: 'bg-sky-500 text-white hover:bg-sky-500/80', - ghost: 'hover:bg-accent', + primary: 'bg-gradient-to-r from-tomato-600 to-rose-500 text-white font-semibold hover:opacity-90', + destructive: 'bg-tomato-500 text-white font-semibold hover:bg-tomato-500/90', + outline: + 'text-slate-900 border border-slate-400 hover:bg-slate-200 px-2.5 py-1 bg-transparent font-semibold', + secondary: 'bg-sky-500 text-white hover:bg-sky-500/80 font-semibold', + ghost: 'hover:opacity-80 bg-transparent font-semibold', link: 'text-primary underline-offset-4 hover:underline' } const SIZE_STYLES: Record = { - default: 'px-4 py-2 text-xs', + default: '', sm: 'h-7 px-3 text-xs', md: 'h-9 px-4 text-sm', lg: 'h-11 px-8 text-base' @@ -29,18 +30,18 @@ const SIZE_STYLES: Record = { const SHAPE_STYLES: Record = { pill: 'rounded-full', circle: 'rounded-full aspect-square', - default: 'rounded-md' + default: '' } export interface ButtonProps extends React.ButtonHTMLAttributes { asChild?: boolean variant: Variant size?: Size - shape: Shape + shape?: Shape } const Button = React.forwardRef( - ({className, variant, size, shape, asChild = false, ...props}, ref) => { + ({className, variant, size = 'default', shape = 'default', asChild = false, ...props}, ref) => { const Comp = asChild ? Slot : 'button' return ( number ) { const {submitting, onError, onCompleted, submitMutation} = mutationProps const providerState = Math.random().toString(36).substring(5) @@ -31,10 +33,11 @@ class GoogleClientManager extends GoogleManager { }) const uri = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}` submitMutation() + const top = getOffsetTop?.() || 56 const popup = window.open( uri, 'OAuth', - getOAuthPopupFeatures({width: 356, height: 530, top: 56}) + getOAuthPopupFeatures({width: AUTH_DIALOG_WIDTH, height: 576, top}) ) const closeCheckerId = window.setInterval(() => { if (popup && popup.closed) { diff --git a/packages/client/utils/MicrosoftClientManager.ts b/packages/client/utils/MicrosoftClientManager.ts index faf4c35e0c7..6ae81825b78 100644 --- a/packages/client/utils/MicrosoftClientManager.ts +++ b/packages/client/utils/MicrosoftClientManager.ts @@ -1,11 +1,12 @@ import {RouterProps} from 'react-router' import Atmosphere from '../Atmosphere' +import {AUTH_DIALOG_WIDTH} from '../components/AuthenticationDialog' import {MenuMutationProps} from '../hooks/useMutationProps' import LoginWithMicrosoftMutation from '../mutations/LoginWithMicrosoftMutation' import {LocalStorageKey} from '../types/constEnums' +import MicrosoftManager from './MicrosoftManager' import getAnonymousId from './getAnonymousId' import getOAuthPopupFeatures from './getOAuthPopupFeatures' -import MicrosoftManager from './MicrosoftManager' import makeHref from './makeHref' class MicrosoftClientManager extends MicrosoftManager { @@ -16,7 +17,8 @@ class MicrosoftClientManager extends MicrosoftManager { history: RouterProps['history'], pageParams: string, invitationToken?: string, - loginHint?: string + loginHint?: string, + getOffsetTop?: () => number ) { const {submitting, onError, onCompleted, submitMutation} = mutationProps const providerState = Math.random().toString(36).substring(5) @@ -33,10 +35,11 @@ class MicrosoftClientManager extends MicrosoftManager { window.__ACTION__.microsoftTenantId }/oauth2/v2.0/authorize?${params.toString()}` submitMutation() + const top = getOffsetTop?.() || 56 const popup = window.open( uri, 'OAuth', - getOAuthPopupFeatures({width: 356, height: 530, top: 56}) + getOAuthPopupFeatures({width: AUTH_DIALOG_WIDTH, height: 576, top}) ) const closeCheckerId = window.setInterval(() => { if (popup && popup.closed) { diff --git a/packages/client/utils/getOAuthPopupFeatures.ts b/packages/client/utils/getOAuthPopupFeatures.ts index 18fea4f2a41..599712d413e 100644 --- a/packages/client/utils/getOAuthPopupFeatures.ts +++ b/packages/client/utils/getOAuthPopupFeatures.ts @@ -1,16 +1,15 @@ interface Box { width: number height: number - top: number + top?: number } const getOAuthPopupFeatures = ({width, height, top}: Box) => { const {outerWidth, innerWidth, outerHeight, innerHeight, screenX, screenY} = window const startX = screenX + (outerWidth - innerWidth) / 2 - const startY = screenY + (outerHeight - innerHeight) / 2 + const startY = screenY + (outerHeight - innerHeight) const left = Math.round(startX + (innerWidth - width) / 2) - // 64 is the Parabol header - const topOff = Math.round(startY + (innerHeight - height) / 2 + top) + const topOff = top ? Math.round(startY + top) : Math.round(startY + (innerHeight - height) / 2) return `popup,width=${width},height=${height},left=${left},top=${topOff}` } diff --git a/packages/client/utils/getTokenFromSSO.ts b/packages/client/utils/getTokenFromSSO.ts index 569d51a63d7..e9504136225 100644 --- a/packages/client/utils/getTokenFromSSO.ts +++ b/packages/client/utils/getTokenFromSSO.ts @@ -12,11 +12,11 @@ type ErrorReturnType = { type ReturnType = SucessReturnType | ErrorReturnType -const getTokenFromSSO = (url: string): ReturnType | Promise => { +const getTokenFromSSO = (url: string, top?: number): ReturnType | Promise => { // It's possible we prematurely opened a popup named SSO at the URL about:blank to avoid popup blockers // Calling window.open again will get a reference to that popup // Then, we can update the href to the valid URL - const popup = window.open(url, 'SSO', getOAuthPopupFeatures({width: 385, height: 550, top: 64})) + const popup = window.open(url, 'SSO', getOAuthPopupFeatures({width: 385, height: 576, top})) if (!popup) return {error: 'Failed to open login popup'} popup.location.href = url let closeCheckerId: undefined | number diff --git a/packages/client/utils/makeCheckinGreeting.ts b/packages/client/utils/makeCheckinGreeting.ts index c7618d32e3b..09203e4595f 100644 --- a/packages/client/utils/makeCheckinGreeting.ts +++ b/packages/client/utils/makeCheckinGreeting.ts @@ -40,7 +40,7 @@ const greetings = [ language: 'Chinese' }, { - content: 'Yeoboseyo', + content: 'Annyeong', language: 'Korean' }, { @@ -70,6 +70,42 @@ const greetings = [ { content: 'Lei Ho', language: 'Cantonese' + }, + { + content: 'Salaam', + language: 'Arabic' + }, + { + content: 'Filipino', + language: 'Kumusta' + }, + { + content: 'Pryvit', + language: 'Ukranian' + }, + { + content: 'Habari', + language: 'Swahili' + }, + { + content: 'Vanakkam', + language: 'Tamil' + }, + { + content: 'Chào', + language: 'Vietnamese' + }, + { + content: 'Gamardjoba', + language: 'Georgian' + }, + { + content: 'Selam', + language: 'Amharic' + }, + { + content: 'Iska warran', + language: 'Somalian' } ] @@ -294,7 +330,11 @@ const questions = [ 'If buying groceries were a game, what would be one of the loading screen tips?', 'What’s one of your recent pet peeves?', 'How would your best friend describe you?', - 'Congratulations! You’ve been chosen to represent your country in a global competition. What sport or activity are you doing?' + 'Congratulations! You’ve been chosen to represent your country in a global competition. What sport or activity are you doing?', + 'What’s a small victory you had this week that might seem trivial but was important to you?', + 'If you could have any author, living or dead, write the story of your life, who would it be and why?', + 'Imagine you could teleport to any place in the world for your next meal. Where would you go and what would you eat?', + 'What’s one song that always boosts your mood, no matter how many times you hear it?' ] export const makeCheckinGreeting = (meetingCount: number, seedId = '') => { diff --git a/packages/gql-executor/package.json b/packages/gql-executor/package.json index 5fc6dbb0961..9c8df6f3f95 100644 --- a/packages/gql-executor/package.json +++ b/packages/gql-executor/package.json @@ -1,6 +1,6 @@ { "name": "gql-executor", - "version": "7.20.0", + "version": "7.22.2", "description": "A Stateless GraphQL Executor", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/gqlExecutor#readme", @@ -27,8 +27,8 @@ }, "dependencies": { "dd-trace": "^4.2.0", - "parabol-client": "7.20.0", - "parabol-server": "7.20.0", + "parabol-client": "7.22.2", + "parabol-server": "7.22.2", "undici": "^5.26.2" } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 9053ded2078..fed0f57a2eb 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -2,7 +2,7 @@ "name": "integration-tests", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.20.0", + "version": "7.22.2", "description": "", "main": "index.js", "scripts": { diff --git a/packages/server/__tests__/globalSetup.ts b/packages/server/__tests__/globalSetup.ts index b4cba0d381f..69df2a89fdd 100644 --- a/packages/server/__tests__/globalSetup.ts +++ b/packages/server/__tests__/globalSetup.ts @@ -9,7 +9,7 @@ async function setup() { // so the safety checks will eventually fail if run too much await Promise.all([ - r.table('FailedAuthRequest').delete().run(), + pg.deleteFrom('FailedAuthRequest').execute(), r.table('PasswordResetRequest').delete().run(), pg.deleteFrom('SAMLDomain').where('domain', '=', 'example.com').execute() ]) diff --git a/packages/server/billing/helpers/removeTeamsLimitObjects.ts b/packages/server/billing/helpers/removeTeamsLimitObjects.ts index 2f38ee51122..2f112a11077 100644 --- a/packages/server/billing/helpers/removeTeamsLimitObjects.ts +++ b/packages/server/billing/helpers/removeTeamsLimitObjects.ts @@ -1,20 +1,21 @@ +import getKysely from '../../postgres/getKysely' import {r} from 'rethinkdb-ts' import {RValue} from '../../database/stricterR' import {DataLoaderWorker} from '../../graphql/graphql' import updateNotification from '../../graphql/public/mutations/helpers/updateNotification' const removeTeamsLimitObjects = async (orgId: string, dataLoader: DataLoaderWorker) => { - const removeJobTypes = ['LOCK_ORGANIZATION', 'WARN_ORGANIZATION'] - const removeNotificationTypes = ['TEAMS_LIMIT_EXCEEDED', 'TEAMS_LIMIT_REMINDER'] + const removeJobTypes = ['LOCK_ORGANIZATION', 'WARN_ORGANIZATION'] as const + const removeNotificationTypes = ['TEAMS_LIMIT_EXCEEDED', 'TEAMS_LIMIT_REMINDER'] as const + const pg = getKysely() // Remove team limits jobs and existing notifications const [, updateNotificationsChanges] = await Promise.all([ - r - .table('ScheduledJob') - .getAll(orgId, {index: 'orgId'}) - .filter((row: RValue) => r.expr(removeJobTypes).contains(row('type'))) - .delete() - .run(), + pg + .deleteFrom('ScheduledJob') + .where('orgId', '=', orgId) + .where('type', 'in', removeJobTypes) + .execute(), r .table('Notification') .getAll(orgId, {index: 'orgId'}) diff --git a/packages/server/billing/helpers/teamLimitsCheck.ts b/packages/server/billing/helpers/teamLimitsCheck.ts index b9326c20e7c..31103db959b 100644 --- a/packages/server/billing/helpers/teamLimitsCheck.ts +++ b/packages/server/billing/helpers/teamLimitsCheck.ts @@ -2,32 +2,36 @@ import ms from 'ms' import {Threshold} from 'parabol-client/types/constEnums' // Uncomment for easier testing // import { ThresholdTest as Threshold } from "~/types/constEnums"; +import {sql} from 'kysely' import {r} from 'rethinkdb-ts' import NotificationTeamsLimitExceeded from '../../database/types/NotificationTeamsLimitExceeded' import Organization from '../../database/types/Organization' import scheduleTeamLimitsJobs from '../../database/types/scheduleTeamLimitsJobs' import {DataLoaderWorker} from '../../graphql/graphql' import publishNotification from '../../graphql/public/mutations/helpers/publishNotification' +import getActiveTeamCountByTeamIds from '../../graphql/public/types/helpers/getActiveTeamCountByTeamIds' +import {getFeatureTier} from '../../graphql/types/helpers/getFeatureTier' import {domainHasActiveDeals} from '../../hubSpot/hubSpotApi' -import getPg from '../../postgres/getPg' -import {appendUserFeatureFlagsQuery} from '../../postgres/queries/generated/appendUserFeatureFlagsQuery' +import getKysely from '../../postgres/getKysely' +import getTeamIdsByOrgIds from '../../postgres/queries/getTeamIdsByOrgIds' +import {getBillingLeadersByOrgId} from '../../utils/getBillingLeadersByOrgId' import sendToSentry from '../../utils/sendToSentry' import removeTeamsLimitObjects from './removeTeamsLimitObjects' import sendTeamsLimitEmail from './sendTeamsLimitEmail' -import getTeamIdsByOrgIds from '../../postgres/queries/getTeamIdsByOrgIds' -import getActiveTeamCountByTeamIds from '../../graphql/public/types/helpers/getActiveTeamCountByTeamIds' -import {getBillingLeadersByOrgId} from '../../utils/getBillingLeadersByOrgId' -import {getFeatureTier} from '../../graphql/types/helpers/getFeatureTier' const enableUsageStats = async (userIds: string[], orgId: string) => { + const pg = getKysely() await r .table('OrganizationUser') .getAll(r.args(userIds), {index: 'userId'}) .filter({orgId}) .update({suggestedTier: 'team'}) .run() - - await appendUserFeatureFlagsQuery.run({ids: userIds, flag: 'insights'}, getPg()) + await pg + .updateTable('User') + .set({featureFlags: sql`arr_append_uniq("featureFlags", 'insights')`}) + .where('id', 'in', userIds) + .execute() } const sendWebsiteNotifications = async ( diff --git a/packages/server/database/types/FailedAuthRequest.ts b/packages/server/database/types/FailedAuthRequest.ts index 7f6d165d9a2..fe30f30aafe 100644 --- a/packages/server/database/types/FailedAuthRequest.ts +++ b/packages/server/database/types/FailedAuthRequest.ts @@ -1,20 +1,15 @@ -import generateUID from '../../generateUID' - interface Input { - id?: string ip: string email: string time?: Date } export default class FailedAuthRequest { - id: string ip: string email: string time: Date constructor(input: Input) { - const {id, email, ip, time} = input - this.id = id ?? generateUID() + const {email, ip, time} = input this.email = email this.ip = ip this.time = time ?? new Date() diff --git a/packages/server/database/types/ScheduledJob.ts b/packages/server/database/types/ScheduledJob.ts index 06071851a89..ad42dce95a6 100644 --- a/packages/server/database/types/ScheduledJob.ts +++ b/packages/server/database/types/ScheduledJob.ts @@ -1,11 +1,8 @@ -import generateUID from '../../generateUID' - export type ScheduledJobType = | 'MEETING_STAGE_TIME_LIMIT_END' | 'LOCK_ORGANIZATION' | 'WARN_ORGANIZATION' export default abstract class ScheduledJob { - id = generateUID() protected constructor(public type: ScheduledJobType, public runAt: Date) {} } diff --git a/packages/server/database/types/scheduleTeamLimitsJobs.ts b/packages/server/database/types/scheduleTeamLimitsJobs.ts index 368d75edc6a..427d328e608 100644 --- a/packages/server/database/types/scheduleTeamLimitsJobs.ts +++ b/packages/server/database/types/scheduleTeamLimitsJobs.ts @@ -1,21 +1,23 @@ import ms from 'ms' -import {r} from 'rethinkdb-ts' +import getKysely from '../../postgres/getKysely' import {Threshold} from '../../../client/types/constEnums' import ScheduledTeamLimitsJob from './ScheduledTeamLimitsJob' const scheduleTeamLimitsJobs = async (scheduledLockAt: Date, orgId: string) => { - const scheduledLock = r - .table('ScheduledJob') - .insert(new ScheduledTeamLimitsJob(scheduledLockAt, orgId, 'LOCK_ORGANIZATION')) - .run() + const pg = getKysely() + const scheduledLock = pg + .insertInto('ScheduledJob') + .values(new ScheduledTeamLimitsJob(scheduledLockAt, orgId, 'LOCK_ORGANIZATION')) + .execute() const oneWeekBeforeLock = new Date( scheduledLockAt.getTime() - ms(`${Threshold.FINAL_WARNING_DAYS_BEFORE_LOCK}d`) ) - const scheduledWarn = r - .table('ScheduledJob') - .insert(new ScheduledTeamLimitsJob(oneWeekBeforeLock, orgId, 'WARN_ORGANIZATION')) - .run() + + const scheduledWarn = pg + .insertInto('ScheduledJob') + .values(new ScheduledTeamLimitsJob(oneWeekBeforeLock, orgId, 'WARN_ORGANIZATION')) + .execute() await Promise.all([scheduledLock, scheduledWarn]) } diff --git a/packages/server/fileStorage/FileStoreManager.ts b/packages/server/fileStorage/FileStoreManager.ts index 203d221eb2c..32bb27e2d6d 100644 --- a/packages/server/fileStorage/FileStoreManager.ts +++ b/packages/server/fileStorage/FileStoreManager.ts @@ -1,8 +1,10 @@ import generateUID from '../generateUID' +export type FileAssetDir = 'store' | 'build' + export default abstract class FileStoreManager { - abstract checkExists(fileName: string): Promise - abstract prependPath(partialPath: string): string + abstract checkExists(fileName: string, assetDir?: FileAssetDir): Promise + abstract prependPath(partialPath: string, assetDir?: FileAssetDir): string abstract getPublicFileLocation(fullPath: string): string protected abstract putFile(file: Buffer, fullPath: string): Promise diff --git a/packages/server/fileStorage/GCSManager.ts b/packages/server/fileStorage/GCSManager.ts index a3a1cb4d03e..fe952eed138 100644 --- a/packages/server/fileStorage/GCSManager.ts +++ b/packages/server/fileStorage/GCSManager.ts @@ -2,7 +2,7 @@ import {sign} from 'jsonwebtoken' import mime from 'mime-types' import path from 'path' import {Logger} from '../utils/Logger' -import FileStoreManager from './FileStoreManager' +import FileStoreManager, {FileAssetDir} from './FileStoreManager' interface CloudKey { clientEmail: string @@ -141,18 +141,18 @@ export default class GCSManager extends FileStoreManager { } putBuildFile(file: Buffer, partialPath: string): Promise { - const fullPath = path.join(this.envSubDir, 'build', partialPath) + const fullPath = this.prependPath(partialPath, 'build') return this.putFile(file, fullPath) } - prependPath(partialPath: string) { - return path.join(this.envSubDir, 'store', partialPath) + prependPath(partialPath: string, assetDir: FileAssetDir = 'store') { + return path.join(this.envSubDir, assetDir, partialPath) } getPublicFileLocation(fullPath: string) { return encodeURI(`${this.baseUrl}${fullPath}`) } - async checkExists(partialPath: string) { - const fullPath = encodeURIComponent(this.prependPath(partialPath)) + async checkExists(partialPath: string, assetDir?: FileAssetDir) { + const fullPath = encodeURIComponent(this.prependPath(partialPath, assetDir)) const url = `https://storage.googleapis.com/storage/v1/b/${this.bucket}/o/${fullPath}` const res = await fetch(url) return res.status !== 404 diff --git a/packages/server/fileStorage/S3FileStoreManager.ts b/packages/server/fileStorage/S3FileStoreManager.ts index caace24f9c9..3971ea80eea 100644 --- a/packages/server/fileStorage/S3FileStoreManager.ts +++ b/packages/server/fileStorage/S3FileStoreManager.ts @@ -1,7 +1,7 @@ import {HeadObjectCommand, PutObjectCommand, S3Client} from '@aws-sdk/client-s3' import mime from 'mime-types' import path from 'path' -import FileStoreManager from './FileStoreManager' +import FileStoreManager, {FileAssetDir} from './FileStoreManager' export default class S3Manager extends FileStoreManager { // e.g. development, production @@ -55,8 +55,8 @@ export default class S3Manager extends FileStoreManager { return this.getPublicFileLocation(fullPath) } - prependPath(partialPath: string) { - return path.join(this.envSubDir, 'store', partialPath) + prependPath(partialPath: string, assetDir: FileAssetDir = 'store') { + return path.join(this.envSubDir, assetDir, partialPath) } getPublicFileLocation(fullPath: string) { @@ -64,11 +64,11 @@ export default class S3Manager extends FileStoreManager { } putBuildFile(file: Buffer, partialPath: string): Promise { - const fullPath = path.join(this.envSubDir, 'build', partialPath) + const fullPath = this.prependPath(partialPath, 'build') return this.putFile(file, fullPath) } - async checkExists(key: string) { - const Key = this.prependPath(key) + async checkExists(key: string, assetDir?: FileAssetDir) { + const Key = this.prependPath(key, assetDir) try { await this.s3.send(new HeadObjectCommand({Bucket: this.bucket, Key})) } catch (e) { diff --git a/packages/server/graphql/mutations/helpers/attemptLogin.ts b/packages/server/graphql/mutations/helpers/attemptLogin.ts index 0952c6bd26a..9afe4f20641 100644 --- a/packages/server/graphql/mutations/helpers/attemptLogin.ts +++ b/packages/server/graphql/mutations/helpers/attemptLogin.ts @@ -1,44 +1,56 @@ import bcrypt from 'bcryptjs' +import {sql} from 'kysely' import ms from 'ms' import {AuthenticationError, Threshold} from 'parabol-client/types/constEnums' import sleep from 'parabol-client/utils/sleep' import {AuthIdentityTypeEnum} from '../../../../client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' -import {RDatum} from '../../../database/stricterR' +import getKysely from '../../../postgres/getKysely' import AuthIdentityLocal from '../../../database/types/AuthIdentityLocal' import AuthToken from '../../../database/types/AuthToken' import FailedAuthRequest from '../../../database/types/FailedAuthRequest' import {getUserByEmail} from '../../../postgres/queries/getUsersByEmails' const logFailedLogin = async (ip: string, email: string) => { - const r = await getRethink() + const pg = getKysely() if (ip) { const failedAuthRequest = new FailedAuthRequest({ip, email}) - await r.table('FailedAuthRequest').insert(failedAuthRequest).run() + await pg.insertInto('FailedAuthRequest').values(failedAuthRequest).execute() } } const attemptLogin = async (denormEmail: string, password: string, ip = '') => { - const r = await getRethink() + const pg = getKysely() const yesterday = new Date(Date.now() - ms('1d')) const email = denormEmail.toLowerCase().trim() const existingUser = await getUserByEmail(email) - const {failOnAccount, failOnTime} = await r({ - failOnAccount: r - .table('FailedAuthRequest') - .getAll(ip, {index: 'ip'}) - .filter({email}) - .filter((row: RDatum) => row('time').ge(yesterday)) - .count() - .ge(Threshold.MAX_ACCOUNT_PASSWORD_ATTEMPTS) as unknown as boolean, - failOnTime: r - .table('FailedAuthRequest') - .getAll(ip, {index: 'ip'}) - .filter((row: RDatum) => row('time').ge(yesterday)) - .count() - .ge(Threshold.MAX_DAILY_PASSWORD_ATTEMPTS) as unknown as boolean - }).run() + const {failOnAccount, failOnTime} = (await pg + .with('byAccount', (qb) => + qb + .selectFrom('FailedAuthRequest') + .select((eb) => eb.fn.count('id').as('attempts')) + .where('ip', '=', ip) + .where('email', '=', email) + .where('time', '>=', yesterday) + ) + .with('byTime', (qb) => + qb + .selectFrom('FailedAuthRequest') + .select((eb) => eb.fn.count('id').as('attempts')) + .where('ip', '=', ip) + .where('time', '>=', yesterday) + ) + .selectFrom(['byAccount', 'byTime']) + .select(({ref}) => [ + sql`${ref('byAccount.attempts')} >= ${Threshold.MAX_ACCOUNT_PASSWORD_ATTEMPTS}`.as( + 'failOnAccount' + ), + sql`${ref('byTime.attempts')} >= ${Threshold.MAX_DAILY_PASSWORD_ATTEMPTS}`.as( + 'failOnTime' + ) + ]) + .executeTakeFirst()) as {failOnAccount: boolean; failOnTime: boolean} + if (failOnAccount || failOnTime) { await sleep(1000) // silently fail to trick security researchers diff --git a/packages/server/graphql/mutations/helpers/removeScheduledJobs.ts b/packages/server/graphql/mutations/helpers/removeScheduledJobs.ts index cd6a5eef813..b4931f9ca3b 100644 --- a/packages/server/graphql/mutations/helpers/removeScheduledJobs.ts +++ b/packages/server/graphql/mutations/helpers/removeScheduledJobs.ts @@ -1,8 +1,19 @@ -import getRethink from '../../../database/rethinkDriver' +import {Updateable} from 'kysely' +import {DB} from '../../../postgres/pg' +import getKysely from '../../../postgres/getKysely' -const removeScheduledJobs = async (runAt: Date, filter: {[key: string]: any}) => { - const r = await getRethink() - return r.table('ScheduledJob').getAll(runAt, {index: 'runAt'}).filter(filter).delete().run() +type FilterType = Omit, 'runAt'> + +const removeScheduledJobs = async (runAt: Date, filter?: FilterType) => { + const pg = getKysely() + let query = pg.deleteFrom('ScheduledJob').where('runAt', '=', runAt) + if (filter) { + Object.keys(filter).forEach((key) => { + const value = filter[key as keyof FilterType] + if (value) query = query.where(key as keyof FilterType, '=', value) + }) + } + return query.execute() } export default removeScheduledJobs diff --git a/packages/server/graphql/mutations/resetPassword.ts b/packages/server/graphql/mutations/resetPassword.ts index 8d4f86bc6ae..bc221f06279 100644 --- a/packages/server/graphql/mutations/resetPassword.ts +++ b/packages/server/graphql/mutations/resetPassword.ts @@ -2,6 +2,7 @@ import bcrypt from 'bcryptjs' import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' import {Security, Threshold} from 'parabol-client/types/constEnums' import {AuthIdentityTypeEnum} from '../../../client/types/constEnums' +import getKysely from '../../postgres/getKysely' import getRethink from '../../database/rethinkDriver' import AuthIdentityLocal from '../../database/types/AuthIdentityLocal' import AuthToken from '../../database/types/AuthToken' @@ -37,6 +38,7 @@ const resetPassword = { if (process.env.AUTH_INTERNAL_DISABLED === 'true') { return {error: {message: 'Resetting password is disabled'}} } + const pg = getKysely() const r = await getRethink() const resetRequest = (await r .table('PasswordResetRequest') @@ -73,7 +75,7 @@ const resetPassword = { localIdentity.isEmailVerified = true await Promise.all([ updateUser({identities}, userId), - r.table('FailedAuthRequest').getAll(email, {index: 'email'}).delete().run() + pg.deleteFrom('FailedAuthRequest').where('email', '=', email).execute() ]) context.authToken = new AuthToken({sub: userId, tms, rol}) await blacklistJWT(userId, context.authToken.iat, context.socketId) diff --git a/packages/server/graphql/mutations/setStageTimer.ts b/packages/server/graphql/mutations/setStageTimer.ts index 81d262d220e..40901c6baa0 100644 --- a/packages/server/graphql/mutations/setStageTimer.ts +++ b/packages/server/graphql/mutations/setStageTimer.ts @@ -1,6 +1,7 @@ import {GraphQLFloat, GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import findStageById from 'parabol-client/utils/meetings/findStageById' +import getKysely from '../../postgres/getKysely' import getRethink from '../../database/rethinkDriver' import ScheduledJobMeetingStageTimeLimit from '../../database/types/ScheduledJobMetingStageTimeLimit' import {getUserId, isTeamMember} from '../../utils/authorization' @@ -90,12 +91,13 @@ export default { ? new Date(now.getTime() + timeRemaining - AVG_PING) : newScheduledEndTime } else { + const pg = getKysely() stage.isAsync = true stage.scheduledEndTime = newScheduledEndTime - await r - .table('ScheduledJob') - .insert(new ScheduledJobMeetingStageTimeLimit(newScheduledEndTime, meetingId)) - .run() + await pg + .insertInto('ScheduledJob') + .values(new ScheduledJobMeetingStageTimeLimit(newScheduledEndTime, meetingId)) + .execute() IntegrationNotifier.startTimeLimit(dataLoader, newScheduledEndTime, meetingId, teamId) } } else { diff --git a/packages/server/graphql/private/mutations/addNewFeature.ts b/packages/server/graphql/private/mutations/addNewFeature.ts index 3a5c8fb73d1..b612061ee9d 100644 --- a/packages/server/graphql/private/mutations/addNewFeature.ts +++ b/packages/server/graphql/private/mutations/addNewFeature.ts @@ -1,8 +1,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../../database/rethinkDriver' import generateUID from '../../../generateUID' -import getPg from '../../../postgres/getPg' -import {addUserNewFeatureQuery} from '../../../postgres/queries/generated/addUserNewFeatureQuery' +import getKysely from '../../../postgres/getKysely' import getRedis from '../../../utils/getRedis' import publish from '../../../utils/publish' import sendToSentry from '../../../utils/sendToSentry' @@ -15,6 +14,7 @@ const addNewFeature: MutationResolvers['addNewFeature'] = async ( ) => { const r = await getRethink() const redis = getRedis() + const pg = getKysely() // AUTH const operationId = dataLoader.share() @@ -30,7 +30,7 @@ const addNewFeature: MutationResolvers['addNewFeature'] = async ( } await Promise.all([ r.table('NewFeature').insert(newFeature).run(), - addUserNewFeatureQuery.run({newFeatureId}, getPg()) + pg.updateTable('User').set({newFeatureId}).execute() ]) const onlineUserIds = new Set() diff --git a/packages/server/graphql/private/mutations/loginSAML.ts b/packages/server/graphql/private/mutations/loginSAML.ts index c3137a14205..03584f15300 100644 --- a/packages/server/graphql/private/mutations/loginSAML.ts +++ b/packages/server/graphql/private/mutations/loginSAML.ts @@ -9,14 +9,14 @@ import {USER_PREFERRED_NAME_LIMIT} from '../../../postgres/constants' import getKysely from '../../../postgres/getKysely' import {getUserByEmail} from '../../../postgres/queries/getUsersByEmails' import encodeAuthToken from '../../../utils/encodeAuthToken' +import {isSingleTenantSSO} from '../../../utils/getSAMLURLFromEmail' import {getSSOMetadataFromURL} from '../../../utils/getSSOMetadataFromURL' import {samlXMLValidator} from '../../../utils/samlXMLValidator' +import standardError from '../../../utils/standardError' import bootstrapNewUser from '../../mutations/helpers/bootstrapNewUser' import getSignOnURL from '../../public/mutations/helpers/SAMLHelpers/getSignOnURL' -import {SSORelayState} from '../../queries/SAMLIdP' +import {SSORelayState} from '../../public/queries/SAMLIdP' import {MutationResolvers} from '../resolverTypes' -import standardError from '../../../utils/standardError' -import {isSingleTenantSSO} from '../../../utils/getSAMLURLFromEmail' const serviceProvider = samlify.ServiceProvider({}) samlify.setSchemaValidator(samlXMLValidator) diff --git a/packages/server/graphql/private/mutations/runScheduledJobs.ts b/packages/server/graphql/private/mutations/runScheduledJobs.ts index faee38517ec..aebb1e5eafe 100644 --- a/packages/server/graphql/private/mutations/runScheduledJobs.ts +++ b/packages/server/graphql/private/mutations/runScheduledJobs.ts @@ -1,4 +1,7 @@ +import {Selectable} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import getKysely from '../../../postgres/getKysely' +import {DB} from '../../../postgres/pg' import getRethink from '../../../database/rethinkDriver' import NotificationMeetingStageTimeLimitEnd from '../../../database/types/NotificationMeetingStageTimeLimitEnd' import processTeamsLimitsJob from '../../../database/types/processTeamsLimitsJob' @@ -39,11 +42,11 @@ const processMeetingStageTimeLimits = async ( export type ScheduledJobUnion = ScheduledJobMeetingStageTimeLimit | ScheduledTeamLimitsJob -const processJob = async (job: ScheduledJobUnion, dataLoader: DataLoaderWorker) => { - const r = await getRethink() - const res = await r.table('ScheduledJob').get(job.id).delete().run() +const processJob = async (job: Selectable, dataLoader: DataLoaderWorker) => { + const pg = getKysely() + const res = await pg.deleteFrom('ScheduledJob').where('id', '=', job.id).executeTakeFirst() // prevent duplicates. after this point, we assume the job finishes to completion (ignores server crashes, etc.) - if (res.deleted !== 1) return + if (res.numDeletedRows !== BigInt(1)) return if (job.type === 'MEETING_STAGE_TIME_LIMIT_END') { return processMeetingStageTimeLimits( @@ -60,15 +63,16 @@ const runScheduledJobs: MutationResolvers['runScheduledJobs'] = async ( {seconds}, {dataLoader} ) => { - const r = await getRethink() + const pg = getKysely() const now = new Date() // RESOLUTION const before = new Date(now.getTime() + seconds * 1000) - const upcomingJobs = (await r - .table('ScheduledJob') - .between(r.minval, before, {index: 'runAt'}) - .run()) as ScheduledJobUnion[] + const upcomingJobs = await pg + .selectFrom('ScheduledJob') + .selectAll() + .where('runAt', '<', before) + .execute() upcomingJobs.forEach((job) => { const {runAt} = job diff --git a/packages/server/graphql/private/typeDefs/updateOrgFeatureFlag.graphql b/packages/server/graphql/private/typeDefs/updateOrgFeatureFlag.graphql index fffa8ee086e..211113f9903 100644 --- a/packages/server/graphql/private/typeDefs/updateOrgFeatureFlag.graphql +++ b/packages/server/graphql/private/typeDefs/updateOrgFeatureFlag.graphql @@ -14,7 +14,6 @@ enum OrganizationFeatureFlagsEnum { oneOnOne singleColumnStandups publicTeams - meetingInception kudos aiIcebreakers aiTemplate diff --git a/packages/server/graphql/public/mutations/addReactjiToReactable.ts b/packages/server/graphql/public/mutations/addReactjiToReactable.ts index 1b4161f0335..0a343f921f4 100644 --- a/packages/server/graphql/public/mutations/addReactjiToReactable.ts +++ b/packages/server/graphql/public/mutations/addReactjiToReactable.ts @@ -7,9 +7,6 @@ import {RDatum} from '../../../database/stricterR' import Comment from '../../../database/types/Comment' import {Reactable} from '../../../database/types/Reactable' import Reflection from '../../../database/types/Reflection' -import getPg from '../../../postgres/getPg' -import {appendTeamResponseReactji} from '../../../postgres/queries/generated/appendTeamResponseReactjiQuery' -import {removeTeamResponseReactji} from '../../../postgres/queries/generated/removeTeamResponseReactjiQuery' import {analytics} from '../../../utils/analytics/analytics' import {getUserId} from '../../../utils/authorization' import emojiIds from '../../../utils/emojiIds' @@ -17,14 +14,15 @@ import getGroupedReactjis from '../../../utils/getGroupedReactjis' import publish from '../../../utils/publish' import {GQLContext} from '../../graphql' -import getReactableType from '../../types/getReactableType' -import {ReactableEnumType} from '../../types/ReactableEnum' -import getKysely from '../../../postgres/getKysely' -import {AnyMeeting} from '../../../postgres/types/Meeting' +import {sql} from 'kysely' import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import NotificationKudosReceived from '../../../database/types/NotificationKudosReceived' +import getKysely from '../../../postgres/getKysely' import {TeamPromptResponse} from '../../../postgres/queries/getTeamPromptResponsesByIds' +import {AnyMeeting} from '../../../postgres/types/Meeting' +import {ReactableEnumType} from '../../types/ReactableEnum' +import getReactableType from '../../types/getReactableType' import {MutationResolvers} from '../resolverTypes' -import NotificationKudosReceived from '../../../database/types/NotificationKudosReceived' import publishNotification from './helpers/publishNotification' const rethinkTableLookup = { @@ -129,15 +127,19 @@ const addReactjiToReactable: MutationResolvers['addReactjiToReactable'] = async if (pgLoaderName) { const numberReactableId = TeamPromptResponseId.split(reactableId) if (isRemove) { - await removeTeamResponseReactji.run( - {id: numberReactableId, reactji: {shortname: reactji, userid: viewerId}}, - getPg() - ) + await pg + .updateTable('TeamPromptResponse') + .set({reactjis: sql`array_remove("reactjis", (${reactji},${viewerId})::"Reactji")`}) + .where('id', '=', numberReactableId) + .execute() } else { - await appendTeamResponseReactji.run( - {id: numberReactableId, reactji: {shortname: reactji, userid: viewerId}}, - getPg() - ) + await pg + .updateTable('TeamPromptResponse') + .set({ + reactjis: sql`arr_append_uniq("reactjis", (${reactji},${viewerId})::"Reactji")` + }) + .where('id', '=', numberReactableId) + .execute() } dataLoader.get(pgLoaderName).clear(reactableId) diff --git a/packages/server/graphql/public/mutations/updateFeatureFlag.ts b/packages/server/graphql/public/mutations/updateFeatureFlag.ts index a8b336c6db9..bf7a19cef69 100644 --- a/packages/server/graphql/public/mutations/updateFeatureFlag.ts +++ b/packages/server/graphql/public/mutations/updateFeatureFlag.ts @@ -1,7 +1,7 @@ +import {ValueExpression, sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getPg from '../../../postgres/getPg' -import {appendUserFeatureFlagsQuery} from '../../../postgres/queries/generated/appendUserFeatureFlagsQuery' -import {removeUserFeatureFlagsQuery} from '../../../postgres/queries/generated/removeUserFeatureFlagsQuery' +import getKysely from '../../../postgres/getKysely' +import {DB} from '../../../postgres/pg' import getUsersByDomain from '../../../postgres/queries/getUsersByDomain' import {getUsersByEmails} from '../../../postgres/queries/getUsersByEmails' import IUser from '../../../postgres/types/IUser' @@ -17,6 +17,7 @@ const updateFeatureFlag: MutationResolvers['updateFeatureFlag'] = async ( ) => { const operationId = dataLoader.share() const subOptions = {operationId} + const pg = getKysely() // AUTH const viewerId = getUserId(authToken) @@ -43,9 +44,10 @@ const updateFeatureFlag: MutationResolvers['updateFeatureFlag'] = async ( } const userIds = isUpdatingViewerFlag ? [viewerId] : users.map(({id}) => id) - addFlag - ? await appendUserFeatureFlagsQuery.run({ids: userIds, flag}, getPg()) - : await removeUserFeatureFlagsQuery.run({ids: userIds, flag}, getPg()) + const featureFlags: ValueExpression = addFlag + ? sql`arr_append_uniq("featureFlags", ${flag})` + : sql`array_remove("featureFlags", ${flag})` + await pg.updateTable('User').set({featureFlags}).where('id', 'in', userIds).execute() userIds.forEach((userId) => { const data = {userId} publish(SubscriptionChannel.NOTIFICATION, userId, 'UpdateFeatureFlagPayload', data, subOptions) diff --git a/packages/server/graphql/public/permissions.ts b/packages/server/graphql/public/permissions.ts index 37bb607f152..dd0930cdb35 100644 --- a/packages/server/graphql/public/permissions.ts +++ b/packages/server/graphql/public/permissions.ts @@ -57,7 +57,8 @@ const permissionMap: PermissionMap = { }, Query: { '*': isAuthenticated, - getDemoEntities: rateLimit({perMinute: 5, perHour: 50}) + getDemoEntities: rateLimit({perMinute: 5, perHour: 50}), + SAMLIdP: rateLimit({perMinute: 120, perHour: 3600}) }, Organization: { saml: and(isViewerBillingLeaderSource, isOrgTierSource('enterprise')) diff --git a/packages/server/graphql/public/queries/SAMLIdP.ts b/packages/server/graphql/public/queries/SAMLIdP.ts new file mode 100644 index 00000000000..98b7c0f5e8b --- /dev/null +++ b/packages/server/graphql/public/queries/SAMLIdP.ts @@ -0,0 +1,13 @@ +import getSAMLURLFromEmail from '../../../utils/getSAMLURLFromEmail' +import {QueryResolvers} from '../resolverTypes' + +export interface SSORelayState { + isInvited?: boolean + metadataURL?: string +} + +const SAMLIdP: QueryResolvers['SAMLIdP'] = async (_source, {email, isInvited}, {dataLoader}) => { + return getSAMLURLFromEmail(email, dataLoader, isInvited) +} + +export default SAMLIdP diff --git a/packages/server/graphql/public/typeDefs/Organization.graphql b/packages/server/graphql/public/typeDefs/Organization.graphql index 005ced3a3ab..485adcac9f6 100644 --- a/packages/server/graphql/public/typeDefs/Organization.graphql +++ b/packages/server/graphql/public/typeDefs/Organization.graphql @@ -194,7 +194,6 @@ type OrganizationFeatureFlags { oneOnOne: Boolean! singleColumnStandups: Boolean! publicTeams: Boolean! - meetingInception: Boolean! kudos: Boolean! aiIcebreakers: Boolean! aiTemplate: Boolean! diff --git a/packages/server/graphql/public/typeDefs/Query.graphql b/packages/server/graphql/public/typeDefs/Query.graphql index 1e196c7f36a..ccc8b3e0384 100644 --- a/packages/server/graphql/public/typeDefs/Query.graphql +++ b/packages/server/graphql/public/typeDefs/Query.graphql @@ -20,9 +20,9 @@ type Query { ): VerifiedInvitationPayload! SAMLIdP( """ - the email associated with a SAML login + the email associated with a SAML login. null if instance is SSO-only """ - email: ID! + email: ID """ true if the user was invited, else false diff --git a/packages/server/graphql/public/typeDefs/_legacy.graphql b/packages/server/graphql/public/typeDefs/_legacy.graphql index 79e8797a2a3..75ba9203d1f 100644 --- a/packages/server/graphql/public/typeDefs/_legacy.graphql +++ b/packages/server/graphql/public/typeDefs/_legacy.graphql @@ -7863,7 +7863,7 @@ type StartSprintPokerSuccess { meeting: PokerMeeting! team: Team! teamId: ID! - hasGcalError: Boolean! + hasGcalError: Boolean } """ diff --git a/packages/server/graphql/public/typeDefs/startCheckIn.graphql b/packages/server/graphql/public/typeDefs/startCheckIn.graphql index b21e52d3eb4..8fde46f9b80 100644 --- a/packages/server/graphql/public/typeDefs/startCheckIn.graphql +++ b/packages/server/graphql/public/typeDefs/startCheckIn.graphql @@ -7,7 +7,7 @@ type StartCheckInSuccess { meeting: ActionMeeting! meetingId: ID! team: Team! - hasGcalError: Boolean! + hasGcalError: Boolean } extend type Mutation { diff --git a/packages/server/graphql/public/typeDefs/startRetrospective.graphql b/packages/server/graphql/public/typeDefs/startRetrospective.graphql index 01f0a269fc1..91bb03261c2 100644 --- a/packages/server/graphql/public/typeDefs/startRetrospective.graphql +++ b/packages/server/graphql/public/typeDefs/startRetrospective.graphql @@ -27,7 +27,7 @@ type StartRetrospectiveSuccess { meeting: RetrospectiveMeeting! meetingId: ID! team: Team! - hasGcalError: Boolean! + hasGcalError: Boolean } input CreateGcalEventInput { diff --git a/packages/server/graphql/public/typeDefs/startTeamPrompt.graphql b/packages/server/graphql/public/typeDefs/startTeamPrompt.graphql index 42b2730b109..01bd98483b5 100644 --- a/packages/server/graphql/public/typeDefs/startTeamPrompt.graphql +++ b/packages/server/graphql/public/typeDefs/startTeamPrompt.graphql @@ -35,7 +35,7 @@ type StartTeamPromptSuccess { """ True if there was an error creating the Google Calendar event. False if there was no error or no gcalInput was provided. """ - hasGcalError: Boolean! + hasGcalError: Boolean } input CreateGcalEventInput { diff --git a/packages/server/graphql/public/typeDefs/updateFeatureFlag.graphql b/packages/server/graphql/public/typeDefs/updateFeatureFlag.graphql index fe47e6f40f4..ba9506cf4cd 100644 --- a/packages/server/graphql/public/typeDefs/updateFeatureFlag.graphql +++ b/packages/server/graphql/public/typeDefs/updateFeatureFlag.graphql @@ -4,7 +4,6 @@ A flag to give an individual user super powers enum UserFlagEnum { standups azureDevOps - msTeams insights recurrence noAISummary @@ -21,7 +20,6 @@ The types of flags that give an individual user super powers type UserFeatureFlags { standups: Boolean! azureDevOps: Boolean! - msTeams: Boolean! insights: Boolean! recurrence: Boolean! noAISummary: Boolean! diff --git a/packages/server/graphql/public/types/OrganizationFeatureFlags.ts b/packages/server/graphql/public/types/OrganizationFeatureFlags.ts index efca8fb9406..14f4d59e6a4 100644 --- a/packages/server/graphql/public/types/OrganizationFeatureFlags.ts +++ b/packages/server/graphql/public/types/OrganizationFeatureFlags.ts @@ -13,7 +13,6 @@ const OrganizationFeatureFlags: OrganizationFeatureFlagsResolvers = { oneOnOne: ({oneOnOne}) => !!oneOnOne, publicTeams: ({publicTeams}) => !!publicTeams, singleColumnStandups: ({singleColumnStandups}) => !!singleColumnStandups, - meetingInception: ({meetingInception}) => !!meetingInception, kudos: ({kudos}) => !!kudos, aiIcebreakers: ({aiIcebreakers}) => !!aiIcebreakers, aiTemplate: ({aiTemplate}) => !!aiTemplate, diff --git a/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts b/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts index 5005dd506c9..4ba4baef9be 100644 --- a/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts +++ b/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts @@ -13,8 +13,7 @@ const StartRetrospectiveSuccess: StartRetrospectiveSuccessResolvers = { }, team: ({teamId}, _args: unknown, {dataLoader}) => { return dataLoader.get('teams').loadNonNull(teamId) - }, - hasGcalError: ({hasGcalError}) => !!hasGcalError + } } export default StartRetrospectiveSuccess diff --git a/packages/server/graphql/public/types/UserFeatureFlags.ts b/packages/server/graphql/public/types/UserFeatureFlags.ts index 84bdbd8f5ec..806a33b7c8a 100644 --- a/packages/server/graphql/public/types/UserFeatureFlags.ts +++ b/packages/server/graphql/public/types/UserFeatureFlags.ts @@ -2,7 +2,6 @@ import {UserFeatureFlagsResolvers} from '../resolverTypes' const UserFeatureFlags: UserFeatureFlagsResolvers = { azureDevOps: ({azureDevOps}) => !!azureDevOps, - msTeams: ({msTeams}) => !!msTeams, insights: ({insights}) => !!insights, noAISummary: ({noAISummary}) => !!noAISummary, noMeetingHistoryLimit: ({noMeetingHistoryLimit}) => !!noMeetingHistoryLimit, diff --git a/packages/server/graphql/queries/SAMLIdP.ts b/packages/server/graphql/queries/SAMLIdP.ts deleted file mode 100644 index 114b81958ea..00000000000 --- a/packages/server/graphql/queries/SAMLIdP.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {GraphQLBoolean, GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' -import getSAMLURLFromEmail from '../../utils/getSAMLURLFromEmail' -import rateLimit from '../rateLimit' - -export interface SSOParams { - RelayState: string -} - -export interface SSORelayState { - isInvited?: boolean - metadataURL?: string -} - -const SAMLIdP = { - args: { - email: { - type: new GraphQLNonNull(GraphQLID), - description: 'the email associated with a SAML login' - }, - isInvited: { - type: GraphQLBoolean, - description: 'true if the user was invited, else false' - } - }, - type: GraphQLString, - resolve: rateLimit({perMinute: 120, perHour: 3600})( - async (_source: unknown, {email, isInvited}, {dataLoader}) => { - return getSAMLURLFromEmail(email, dataLoader, isInvited) - } - ) -} - -export default SAMLIdP diff --git a/packages/server/graphql/rootQuery.ts b/packages/server/graphql/rootQuery.ts index 567516058ee..77b7249de74 100644 --- a/packages/server/graphql/rootQuery.ts +++ b/packages/server/graphql/rootQuery.ts @@ -2,7 +2,6 @@ import {GraphQLNonNull, GraphQLObjectType} from 'graphql' import {getUserId} from '../utils/authorization' import {GQLContext} from './graphql' import massInvitation from './queries/massInvitation' -import SAMLIdP from './queries/SAMLIdP' import verifiedInvitation from './queries/verifiedInvitation' import User from './types/User' @@ -18,7 +17,6 @@ export default new GraphQLObjectType({ } }, massInvitation, - verifiedInvitation, - SAMLIdP + verifiedInvitation }) }) diff --git a/packages/server/graphql/types/Organization.ts b/packages/server/graphql/types/Organization.ts index 18055a06e5b..2e51054649d 100644 --- a/packages/server/graphql/types/Organization.ts +++ b/packages/server/graphql/types/Organization.ts @@ -7,7 +7,12 @@ import { GraphQLObjectType, GraphQLString } from 'graphql' -import {getUserId, isSuperUser, isUserBillingLeader} from '../../utils/authorization' +import { + getUserId, + isSuperUser, + isUserBillingLeader, + isUserOrgAdmin +} from '../../utils/authorization' import {GQLContext} from '../graphql' import getActiveTeamCountByOrgIds from '../public/types/helpers/getActiveTeamCountByOrgIds' import {resolveForBillingLeaders} from '../resolvers' @@ -45,12 +50,20 @@ const Organization: GraphQLObjectType = new GraphQLObjectType { const viewerId = getUserId(authToken) return isUserBillingLeader(viewerId, orgId, dataLoader) } }, + isOrgAdmin: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'true if the viewer holds the the org admin role on the org', + resolve: async ({id: orgId}, _args: unknown, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + return isUserOrgAdmin(viewerId, orgId, dataLoader) + } + }, name: { type: new GraphQLNonNull(GraphQLString), description: 'The name of the organization' diff --git a/packages/server/graphql/types/UserFlagEnum.ts b/packages/server/graphql/types/UserFlagEnum.ts index 94827237ab3..09c6c59140a 100644 --- a/packages/server/graphql/types/UserFlagEnum.ts +++ b/packages/server/graphql/types/UserFlagEnum.ts @@ -5,7 +5,6 @@ const UserFlagEnum = new GraphQLEnumType({ description: 'A flag to give an individual user super powers', values: { azureDevOps: {}, - msTeams: {}, noAISummary: {}, noMeetingHistoryLimit: {}, adHocTeams: {}, diff --git a/packages/server/initPublicPath.ts b/packages/server/initPublicPath.ts new file mode 100644 index 00000000000..79d626cf1e3 --- /dev/null +++ b/packages/server/initPublicPath.ts @@ -0,0 +1,16 @@ +import appOrigin from './appOrigin' + +declare let __webpack_public_path__: string +declare const __PRODUCTION__: boolean + +const {CDN_BASE_URL, SOCKET_PORT} = process.env + +if (CDN_BASE_URL) { + // pushToCDN#pushServerAssetsToCDN ensures all assets will be available on the CDN + __webpack_public_path__ = `${CDN_BASE_URL.replace(/^\/{2,}/, 'https://')}/build/` +} else { + const url = new URL('/static/', appOrigin) + // the webpack dev server uses /static on PORT, so fetch assets at SOCKET_PORT + url.port = __PRODUCTION__ ? url.port : SOCKET_PORT! + __webpack_public_path__ = url.toString() +} diff --git a/packages/server/jiraImagesHandler.ts b/packages/server/jiraImagesHandler.ts index 64d7c787b83..8aea88d2cdb 100644 --- a/packages/server/jiraImagesHandler.ts +++ b/packages/server/jiraImagesHandler.ts @@ -1,5 +1,3 @@ -import {promises as fsp} from 'fs' -import path from 'path' import {HttpRequest, HttpResponse} from 'uWebSockets.js' import jiraPlaceholder from '../../static/images/illustrations/imageNotFound.png' import sleep from '../client/utils/sleep' @@ -31,9 +29,12 @@ const getImageFromCache = async ( let jiraPlaceholderBuffer: Buffer | undefined const servePlaceholderImage = async (res: HttpResponse) => { if (!jiraPlaceholderBuffer) { - jiraPlaceholderBuffer = await fsp.readFile( - path.join(__dirname, jiraPlaceholder.slice(__webpack_public_path__.length)) - ) + try { + const res = await fetch(jiraPlaceholder) + jiraPlaceholderBuffer = Buffer.from(await res.arrayBuffer()) + } catch (e) { + console.error('Jira Placeholder image could not be fetched', e) + } } res.writeStatus('200').writeHeader('Content-Type', 'image/png').end(jiraPlaceholderBuffer) } diff --git a/packages/server/package.json b/packages/server/package.json index 1d09f43fca0..4fa8018c480 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.20.0", + "version": "7.22.2", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -67,7 +67,7 @@ "style-loader": "2.0.0", "sucrase": "^3.32.0", "ts-jest": "^29.1.0", - "ts-node": "^8.6.2", + "ts-node": "10.9.2", "typescript": "^5.3.3", "url-loader": "4.1.1", "vscode-apollo-relay": "^1.5.0", @@ -103,7 +103,7 @@ "fast-xml-parser": "^4.2.7", "googleapis": "^118.0.0", "graphql": "15.7.2", - "graphql-jit": "^0.7.4", + "graphql-jit": "^0.8.4", "graphql-middleware": "^6.1.18", "graphql-relay": "^0.10.0", "graphql-shield": "^7.5.0", @@ -123,8 +123,8 @@ "nodemailer": "^6.9.9", "oauth-1.0a": "^2.2.6", "openai": "^4.24.1", - "oy-vey": "^0.11.0", - "parabol-client": "7.20.0", + "oy-vey": "^0.12.1", + "parabol-client": "7.22.2", "pg": "^8.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/packages/server/postgres/migrations/1693991480688_truncateReflectPromptIds.ts b/packages/server/postgres/migrations/1693991480688_truncateReflectPromptIds.ts index f2f5d42472d..a17dd2f0de1 100644 --- a/packages/server/postgres/migrations/1693991480688_truncateReflectPromptIds.ts +++ b/packages/server/postgres/migrations/1693991480688_truncateReflectPromptIds.ts @@ -1,5 +1,4 @@ -import {Client} from 'pg' -import {r} from 'rethinkdb-ts' +import {r, RDatum} from 'rethinkdb-ts' import connectRethinkDB from '../../database/connectRethinkDB' /** @@ -12,13 +11,13 @@ export async function up() { .insert( r .table('ReflectPrompt') - .filter((row) => row('id').count().gt(100)) - .map((row) => row.merge({id: row('id').slice(0, 100)})) + .filter((row: RDatum) => row('id').count().gt(100)) + .map((row: RDatum) => row.merge({id: row('id').slice(0, 100)})) ) .run() await r .table('ReflectPrompt') - .filter((row) => row('id').count().gt(100)) + .filter((row: RDatum) => row('id').count().gt(100)) .delete() .run() await r.getPoolMaster()?.drain() diff --git a/packages/server/postgres/migrations/1694191002164_migrateSAML.ts b/packages/server/postgres/migrations/1694191002164_migrateSAML.ts index f8824407f67..1b2a41bfbc1 100644 --- a/packages/server/postgres/migrations/1694191002164_migrateSAML.ts +++ b/packages/server/postgres/migrations/1694191002164_migrateSAML.ts @@ -1,6 +1,6 @@ import {Kysely, PostgresDialect, sql} from 'kysely' import {Client} from 'pg' -import {r} from 'rethinkdb-ts' +import {r, RDatum} from 'rethinkdb-ts' import getPg from '../getPg' import getPgConfig from '../getPgConfig' @@ -57,7 +57,7 @@ export async function up() { await r .table('SAML') .update( - (saml) => ({ + (saml: RDatum) => ({ orgId: r .table('Organization') .getAll(r.args(saml('domains')), {index: 'activeDomain'}) @@ -78,7 +78,7 @@ export async function up() { const nextSAMLDomains = [] as {domain: string; samlId: string}[] existingSAMLs.forEach((saml) => { - saml.domains.forEach((domain) => { + saml.domains.forEach((domain: any) => { nextSAMLDomains.push({domain, samlId: saml.id}) }) }) diff --git a/packages/server/postgres/migrations/1698265572466_renameEmailVerificationSegmentIdToPseudoId.ts b/packages/server/postgres/migrations/1698265572466_renameEmailVerificationSegmentIdToPseudoId.ts index c6cc5c2659d..694fa70b2fb 100644 --- a/packages/server/postgres/migrations/1698265572466_renameEmailVerificationSegmentIdToPseudoId.ts +++ b/packages/server/postgres/migrations/1698265572466_renameEmailVerificationSegmentIdToPseudoId.ts @@ -1,4 +1,4 @@ -import {r} from 'rethinkdb-ts' +import {r, RDatum} from 'rethinkdb-ts' const connectRethinkDB = async () => { const {hostname: host, port, pathname} = new URL(process.env.RETHINKDB_URL!) @@ -13,7 +13,7 @@ export async function up() { await connectRethinkDB() await r .table('EmailVerification') - .replace((row) => row.without('segmentId').merge({pseudoId: row('segmentId')})) + .replace((row: RDatum) => row.without('segmentId').merge({pseudoId: row('segmentId')})) .run() } @@ -21,6 +21,6 @@ export async function down() { await connectRethinkDB() await r .table('EmailVerification') - .replace((row) => row.without('pseudoId').merge({segmentId: row('pseudoId')})) + .replace((row: RDatum) => row.without('pseudoId').merge({segmentId: row('pseudoId')})) .run() } diff --git a/packages/server/postgres/migrations/1709551949071_makeAllTemplatesFree.ts b/packages/server/postgres/migrations/1709551949071_makeAllTemplatesFree.ts new file mode 100644 index 00000000000..25bc6553255 --- /dev/null +++ b/packages/server/postgres/migrations/1709551949071_makeAllTemplatesFree.ts @@ -0,0 +1,20 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query( + ` + UPDATE "MeetingTemplate" + SET "isFree" = true + WHERE "orgId" = 'aGhostOrg' + ` + ) + + await client.end() +} + +export async function down() { + // noop +} diff --git a/packages/server/postgres/migrations/1709927369000_addFailedAuthRequest.ts b/packages/server/postgres/migrations/1709927369000_addFailedAuthRequest.ts new file mode 100644 index 00000000000..b58461c7b94 --- /dev/null +++ b/packages/server/postgres/migrations/1709927369000_addFailedAuthRequest.ts @@ -0,0 +1,28 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + CREATE TABLE "FailedAuthRequest" ( + "id" INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "email" "citext" NOT NULL, + "ip" "inet" NOT NULL, + "time" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL + ); + + CREATE INDEX IF NOT EXISTS "idx_FailedAuthRequest_email" ON "FailedAuthRequest"("email"); + CREATE INDEX IF NOT EXISTS "idx_FailedAuthRequest_ip" ON "FailedAuthRequest"("ip"); + `) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "FailedAuthRequest"; + `) + await client.end() +} diff --git a/packages/server/postgres/migrations/1709927822000_addScheduledJob.ts b/packages/server/postgres/migrations/1709927822000_addScheduledJob.ts new file mode 100644 index 00000000000..dffd9217014 --- /dev/null +++ b/packages/server/postgres/migrations/1709927822000_addScheduledJob.ts @@ -0,0 +1,38 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'ScheduledJobTypeEnum') THEN + EXECUTE 'CREATE TYPE "ScheduledJobTypeEnum" AS ENUM (''MEETING_STAGE_TIME_LIMIT_END'', ''LOCK_ORGANIZATION'', ''WARN_ORGANIZATION'')'; + END IF; + END $$; + + CREATE TABLE "ScheduledJob" ( + "id" SERIAL PRIMARY KEY, + "runAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + "type" "ScheduledJobTypeEnum" NOT NULL, + "orgId" VARCHAR(100), + "meetingId" VARCHAR(100) + ); + + CREATE INDEX IF NOT EXISTS "idx_ScheduledJob_orgId" ON "ScheduledJob"("orgId"); + CREATE INDEX IF NOT EXISTS "idx_ScheduledJob_runAt" ON "ScheduledJob"("runAt"); + CREATE INDEX IF NOT EXISTS "idx_ScheduledJob_type" ON "ScheduledJob"("type"); + `) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "ScheduledJob"; + DROP TYPE IF EXISTS "ScheduledJobTypeEnum"; + `) + await client.end() +} diff --git a/packages/server/postgres/migrations/1709927835000_moveScheduledJob.ts b/packages/server/postgres/migrations/1709927835000_moveScheduledJob.ts new file mode 100644 index 00000000000..1a1c6950d64 --- /dev/null +++ b/packages/server/postgres/migrations/1709927835000_moveScheduledJob.ts @@ -0,0 +1,59 @@ +import {FirstParam} from 'parabol-client/types/generics' +import {Client} from 'pg' +import {r} from 'rethinkdb-ts' +import getPgConfig from '../getPgConfig' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPgp from '../getPgp' + +export async function up() { + await connectRethinkDB() + const {pgp, pg} = getPgp() + const batchSize = 1000 + + const columnSet = new pgp.helpers.ColumnSet( + ['runAt', 'type', {name: 'orgId', def: null}, {name: 'meetingId', def: null}], + {table: 'ScheduledJob'} + ) + + const getNextData = async (leftBoundCursor: Date | undefined) => { + const startAt = leftBoundCursor || r.minval + const nextBatch = await r + .table('ScheduledJob') + .between(startAt, r.maxval, {index: 'runAt', leftBound: 'open'}) + .orderBy({index: 'runAt'}) + .limit(batchSize) + .run() + if (nextBatch.length === 0) return null + if (nextBatch.length < batchSize) return nextBatch + const lastItem = nextBatch.pop() + const lastMatchingRunAt = nextBatch.findLastIndex((item) => item.runAt !== lastItem!.runAt) + if (lastMatchingRunAt === -1) { + throw new Error( + 'batchSize is smaller than the number of items that share the same cursor. Increase batchSize' + ) + } + return nextBatch.slice(0, lastMatchingRunAt) + } + + await pg.tx('ScheduledJob', (task) => { + const fetchAndProcess: FirstParam = async ( + _index, + leftBoundCursor: undefined | Date + ) => { + const nextData = await getNextData(leftBoundCursor) + if (!nextData) return undefined + const insert = pgp.helpers.insert(nextData, columnSet) + await task.none(insert) + return nextData.at(-1)!.runAt + } + return task.sequence(fetchAndProcess) + }) + await r.getPoolMaster()?.drain() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(`DELETE FROM "ScheduledJob"`) + await client.end() +} diff --git a/packages/server/postgres/queries/archiveTeamsByTeamIds.ts b/packages/server/postgres/queries/archiveTeamsByTeamIds.ts deleted file mode 100644 index 7ba702c37a2..00000000000 --- a/packages/server/postgres/queries/archiveTeamsByTeamIds.ts +++ /dev/null @@ -1,17 +0,0 @@ -import getPg from '../../postgres/getPg' -import { - archiveTeamsByTeamIdsQuery, - IArchiveTeamsByTeamIdsQueryParams -} from '../../postgres/queries/generated/archiveTeamsByTeamIdsQuery' - -const archiveTeamsByTeamIds = async (teamIds: string | string[]) => { - teamIds = typeof teamIds === 'string' ? [teamIds] : teamIds - return archiveTeamsByTeamIdsQuery.run( - { - ids: teamIds - } as IArchiveTeamsByTeamIdsQueryParams, - getPg() - ) -} - -export default archiveTeamsByTeamIds diff --git a/packages/server/postgres/queries/src/addUserNewFeatureQuery.sql b/packages/server/postgres/queries/src/addUserNewFeatureQuery.sql deleted file mode 100644 index b91f680f7b5..00000000000 --- a/packages/server/postgres/queries/src/addUserNewFeatureQuery.sql +++ /dev/null @@ -1,6 +0,0 @@ -/* - @name addUserNewFeatureQuery -*/ -UPDATE "User" SET - "newFeatureId" = :newFeatureId -; diff --git a/packages/server/postgres/queries/src/appendTeamResponseReactjiQuery.sql b/packages/server/postgres/queries/src/appendTeamResponseReactjiQuery.sql deleted file mode 100644 index 7e0875c3857..00000000000 --- a/packages/server/postgres/queries/src/appendTeamResponseReactjiQuery.sql +++ /dev/null @@ -1,7 +0,0 @@ -/* - @name appendTeamResponseReactji - @param reactji -> (shortname, userid) -*/ -UPDATE "TeamPromptResponse" SET - "reactjis" = arr_append_uniq("reactjis", (:reactji)::"Reactji") -WHERE id = :id; diff --git a/packages/server/postgres/queries/src/appendUserFeatureFlagsQuery.sql b/packages/server/postgres/queries/src/appendUserFeatureFlagsQuery.sql deleted file mode 100644 index 4b155286fe4..00000000000 --- a/packages/server/postgres/queries/src/appendUserFeatureFlagsQuery.sql +++ /dev/null @@ -1,7 +0,0 @@ -/* - @name appendUserFeatureFlagsQuery - @param ids -> (...) -*/ -UPDATE "User" SET - "featureFlags" = arr_append_uniq("featureFlags", :flag) -WHERE id IN :ids; diff --git a/packages/server/postgres/queries/src/appendUserTmsQuery.sql b/packages/server/postgres/queries/src/appendUserTmsQuery.sql deleted file mode 100644 index 9bca7883e64..00000000000 --- a/packages/server/postgres/queries/src/appendUserTmsQuery.sql +++ /dev/null @@ -1,6 +0,0 @@ -/* - @name appendUserTmsQuery -*/ -UPDATE "User" SET - tms = arr_append_uniq(tms, :teamId) -WHERE id = :id; diff --git a/packages/server/postgres/queries/src/archiveTeamsByTeamIdsQuery.sql b/packages/server/postgres/queries/src/archiveTeamsByTeamIdsQuery.sql deleted file mode 100644 index aa2d38a8270..00000000000 --- a/packages/server/postgres/queries/src/archiveTeamsByTeamIdsQuery.sql +++ /dev/null @@ -1,10 +0,0 @@ -/* - @name archiveTeamsByTeamIdsQuery - @param ids -> (...) -*/ -UPDATE "Team" SET - "isArchived" = true -WHERE - id IN :ids AND - "isArchived" = false -RETURNING *; diff --git a/packages/server/postgres/queries/src/removeTeamResponseReactjiQuery.sql b/packages/server/postgres/queries/src/removeTeamResponseReactjiQuery.sql deleted file mode 100644 index d86f4d852b8..00000000000 --- a/packages/server/postgres/queries/src/removeTeamResponseReactjiQuery.sql +++ /dev/null @@ -1,7 +0,0 @@ -/* - @name removeTeamResponseReactji - @param reactji -> (shortname, userid) -*/ -UPDATE "TeamPromptResponse" SET - "reactjis" = array_remove("reactjis", (:reactji)::"Reactji") -WHERE id = :id; diff --git a/packages/server/postgres/queries/src/removeUserFeatureFlagsQuery.sql b/packages/server/postgres/queries/src/removeUserFeatureFlagsQuery.sql deleted file mode 100644 index 8db6a6fe0bd..00000000000 --- a/packages/server/postgres/queries/src/removeUserFeatureFlagsQuery.sql +++ /dev/null @@ -1,12 +0,0 @@ -/* - @name removeUserFeatureFlagsQuery - @param ids -> (...) -*/ -UPDATE "User" -SET "featureFlags" = array_remove("featureFlags", x) -FROM ( -SELECT id, unnest("featureFlags") as x -FROM "User" -WHERE id IN :ids -) sub -WHERE "User".id = sub.id AND x = :flag; diff --git a/packages/server/safeMutations/addTeamIdToTMS.ts b/packages/server/safeMutations/addTeamIdToTMS.ts index 0da65f0e66e..faa223723d3 100644 --- a/packages/server/safeMutations/addTeamIdToTMS.ts +++ b/packages/server/safeMutations/addTeamIdToTMS.ts @@ -1,8 +1,13 @@ -import getPg from '../postgres/getPg' -import {appendUserTmsQuery} from '../postgres/queries/generated/appendUserTmsQuery' +import {sql} from 'kysely' +import getKysely from '../postgres/getKysely' const addTeamIdToTMS = async (userId: string, teamId: string) => { - return appendUserTmsQuery.run({id: userId, teamId}, getPg()) + const pg = getKysely() + return pg + .updateTable('User') + .set({tms: sql`arr_append_uniq("tms", ${teamId})`}) + .where('id', '=', userId) + .execute() } export default addTeamIdToTMS diff --git a/packages/server/safeMutations/safeArchiveTeam.ts b/packages/server/safeMutations/safeArchiveTeam.ts index 9ed81c4ce7b..5283349838d 100644 --- a/packages/server/safeMutations/safeArchiveTeam.ts +++ b/packages/server/safeMutations/safeArchiveTeam.ts @@ -1,11 +1,12 @@ import getRethink from '../database/rethinkDriver' import {RDatum} from '../database/stricterR' import {DataLoaderWorker} from '../graphql/graphql' -import archiveTeamsByTeamIds from '../postgres/queries/archiveTeamsByTeamIds' +import getKysely from '../postgres/getKysely' import removeUserTms from '../postgres/queries/removeUserTms' const safeArchiveTeam = async (teamId: string, dataLoader: DataLoaderWorker) => { const r = await getRethink() + const pg = getKysely() const now = new Date() const userIds = await r .table('TeamMember') @@ -34,10 +35,15 @@ const safeArchiveTeam = async (teamId: string, dataLoader: DataLoaderWorker) => )('changes')('new_val')('id') .default([]) as unknown as string[] }).run(), - archiveTeamsByTeamIds(teamId) + pg + .updateTable('Team') + .set({isArchived: true}) + .where('id', '=', teamId) + .returningAll() + .executeTakeFirst() ]) - return {...rethinkResult, team: pgResult[0] ?? null, users} + return {...rethinkResult, team: pgResult ?? null, users} } export default safeArchiveTeam diff --git a/packages/server/selfHostedHandler.ts b/packages/server/selfHostedHandler.ts index 4174e125f23..2387a01d87e 100644 --- a/packages/server/selfHostedHandler.ts +++ b/packages/server/selfHostedHandler.ts @@ -21,7 +21,7 @@ const selfHostedHandler = async (res: HttpResponse, req: HttpRequest) => { try { stats = fs.statSync(url) } catch (e) { - res.writeStatus('404').end() + res.cork(() => res.writeStatus('404').end()) return } const {size} = stats diff --git a/packages/server/types/modules.d.ts b/packages/server/types/modules.d.ts index 14b0e362636..c79e5a0b011 100644 --- a/packages/server/types/modules.d.ts +++ b/packages/server/types/modules.d.ts @@ -26,7 +26,7 @@ declare module 'object-hash' declare module 'string-score' declare const __APP_VERSION__: string -declare const __PRODUCTION__: string +declare const __PRODUCTION__: boolean declare const __SOCKET_PORT__: string declare const __webpack_public_path__: string diff --git a/packages/server/utils/authorization.ts b/packages/server/utils/authorization.ts index 4537f03e77c..dd873e6c885 100644 --- a/packages/server/utils/authorization.ts +++ b/packages/server/utils/authorization.ts @@ -4,6 +4,7 @@ import AuthToken from '../database/types/AuthToken' import OrganizationUser from '../database/types/OrganizationUser' import {DataLoaderWorker} from '../graphql/graphql' import {RDatum} from '../database/stricterR' +import {OrgUserRole} from '../database/types/OrganizationUser' export const getUserId = (authToken: any) => { return authToken && typeof authToken === 'object' ? (authToken.sub as string) : '' @@ -51,10 +52,12 @@ export const isTeamLead = async (userId: string, teamId: string, dataLoader: Dat interface Options { clearCache?: boolean } -export const isUserBillingLeader = async ( + +const isUserAnyRoleIn = async ( userId: string, orgId: string, dataLoader: DataLoaderWorker, + roles: OrgUserRole[], options?: Options ) => { const organizationUser = await dataLoader @@ -63,9 +66,23 @@ export const isUserBillingLeader = async ( if (options && options.clearCache) { dataLoader.get('organizationUsersByUserId').clear(userId) } - return organizationUser - ? organizationUser.role === 'BILLING_LEADER' || organizationUser.role === 'ORG_ADMIN' - : false + return organizationUser && organizationUser.role ? roles.includes(organizationUser.role) : false +} +export const isUserBillingLeader = async ( + userId: string, + orgId: string, + dataLoader: DataLoaderWorker, + options?: Options +) => { + return isUserAnyRoleIn(userId, orgId, dataLoader, ['BILLING_LEADER', 'ORG_ADMIN'], options) +} +export const isUserOrgAdmin = async ( + userId: string, + orgId: string, + dataLoader: DataLoaderWorker, + options?: Options +) => { + return isUserAnyRoleIn(userId, orgId, dataLoader, ['ORG_ADMIN'], options) } export const isUserInOrg = async (userId: string, orgId: string, dataLoader: DataLoaderWorker) => { diff --git a/packages/server/utils/getSAMLURLFromEmail.ts b/packages/server/utils/getSAMLURLFromEmail.ts index 896a479b483..feb6d9a3075 100644 --- a/packages/server/utils/getSAMLURLFromEmail.ts +++ b/packages/server/utils/getSAMLURLFromEmail.ts @@ -19,12 +19,10 @@ const urlWithRelayState = (url: string, isInvited?: boolean | null) => { } const getSAMLURLFromEmail = async ( - email: string, + email: string | null | undefined, dataLoader: DataLoaderWorker, isInvited?: boolean | null ) => { - const domainName = getSSODomainFromEmail(email) - if (!domainName) return null if (isSingleTenantSSO) { // For PPMI use const pg = getKysely() @@ -38,6 +36,10 @@ const getSAMLURLFromEmail = async ( if (!instanceURL) return null return urlWithRelayState(instanceURL, isInvited) } + if (!email) return null + const domainName = getSSODomainFromEmail(email) + if (!domainName) return null + const saml = await dataLoader.get('samlByDomain').load(domainName) if (!saml) return null const {url} = saml diff --git a/packages/server/utils/serveStatic.ts b/packages/server/utils/serveStatic.ts index 2448123077b..4ff491881da 100644 --- a/packages/server/utils/serveStatic.ts +++ b/packages/server/utils/serveStatic.ts @@ -18,9 +18,9 @@ const getProjectRoot = () => { const PROJECT_ROOT = getProjectRoot() const staticPaths = { [path.join(PROJECT_ROOT, 'build')]: true, - [path.join(PROJECT_ROOT, 'dist')]: !__PRODUCTION__, - [path.join(PROJECT_ROOT, 'static')]: !__PRODUCTION__, - [path.join(PROJECT_ROOT, 'dev', 'dll')]: !__PRODUCTION__ + // publish server assets at /static + [path.join(PROJECT_ROOT, 'dist')]: __PRODUCTION__, + [path.join(PROJECT_ROOT, 'dev')]: !__PRODUCTION__ } const staticServer = new StaticServer({staticPaths}) diff --git a/packages/server/utils/uwsGetIP.ts b/packages/server/utils/uwsGetIP.ts index 0151cbd55f5..034b266a3c9 100644 --- a/packages/server/utils/uwsGetIP.ts +++ b/packages/server/utils/uwsGetIP.ts @@ -1,7 +1,7 @@ import {HttpRequest, HttpResponse} from 'uWebSockets.js' const uwsGetIP = (res: HttpResponse, req: HttpRequest) => { - const clientIp = req.getHeader('x-forwarded-for') + const clientIp = req.getHeader('x-forwarded-for')?.split(',')[0] if (clientIp) return clientIp // returns ipv6 e.g. '0000:0000:0000:0000:0000:ffff:ac11:0001' return Buffer.from(res.getRemoteAddressAsText()).toString() diff --git a/scripts/toolboxSrc/applyEnvVarsToClientAssets.ts b/scripts/toolboxSrc/applyEnvVarsToClientAssets.ts index 76f61d59878..1723bab9515 100644 --- a/scripts/toolboxSrc/applyEnvVarsToClientAssets.ts +++ b/scripts/toolboxSrc/applyEnvVarsToClientAssets.ts @@ -5,35 +5,33 @@ import logo192 from '../../static/images/brand/mark-cropped-192.png' import logo512 from '../../static/images/brand/mark-cropped-512.png' import getProjectRoot from '../webpack/utils/getProjectRoot' +declare const __webpack_public_path__: string + const PROJECT_ROOT = getProjectRoot() const clientDir = path.join(PROJECT_ROOT, 'build') -const serverDir = path.join(PROJECT_ROOT, 'dist') - -const getCDNURL = () => { - const {CDN_BASE_URL} = process.env - return CDN_BASE_URL ? `${CDN_BASE_URL}/build` : '/static' -} const rewriteServiceWorker = () => { const skeleton = fs.readFileSync(path.join(clientDir, 'swSkeleton.js'), 'utf-8') - const deploySpecificServiceWorker = skeleton.replaceAll('__PUBLIC_PATH__', getCDNURL()) + const deploySpecificServiceWorker = skeleton.replaceAll( + '__PUBLIC_PATH__', + __webpack_public_path__ + ) fs.writeFileSync(path.join(clientDir, 'sw.js'), deploySpecificServiceWorker) } const writeManifest = () => { // If src is relative, then it will be relative to the manifest location, so manifest.json must be at root / - const cdn = getCDNURL() const manifest = { short_name: 'Parabol', name: 'Parabol', icons: [ { - src: `${cdn}/${logo192}`, + src: logo192, type: 'image/png', sizes: '192x192' }, { - src: `${cdn}/${logo512}`, + src: logo512, type: 'image/png', sizes: '512x512' } @@ -46,9 +44,6 @@ const writeManifest = () => { } const manifestPath = path.join(clientDir, 'manifest.json') fs.writeFileSync(manifestPath, JSON.stringify(manifest)) - // move the referenced icons into the client build - fs.copyFileSync(path.join(serverDir, logo192), path.join(clientDir, logo192)) - fs.copyFileSync(path.join(serverDir, logo512), path.join(clientDir, logo512)) } const rewriteIndexHTML = () => { @@ -63,7 +58,7 @@ const rewriteIndexHTML = () => { sentry: process.env.SENTRY_DSN, slack: process.env.SLACK_CLIENT_ID, stripe: process.env.STRIPE_PUBLISHABLE_KEY, - publicPath: getCDNURL() + '/', + publicPath: __webpack_public_path__, oauth2Redirect: process.env.OAUTH2_REDIRECT, prblIn: process.env.INVITATION_SHORTLINK, AUTH_INTERNAL_ENABLED: process.env.AUTH_INTERNAL_DISABLED !== 'true', @@ -72,7 +67,7 @@ const rewriteIndexHTML = () => { AUTH_SSO_ENABLED: process.env.AUTH_SSO_DISABLED !== 'true', AMPLITUDE_WRITE_KEY: process.env.AMPLITUDE_WRITE_KEY, microsoftTenantId: process.env.MICROSOFT_TENANT_ID, - microsoft: process.env.MICROSOFT_CLIENT_ID, + microsoft: process.env.MICROSOFT_CLIENT_ID } const skeleton = fs.readFileSync(path.join(clientDir, 'skeleton.html'), 'utf8') @@ -82,7 +77,7 @@ const rewriteIndexHTML = () => { const keys = `` const rawHTML = skeleton .replace('', `${noindex}${keys}`) - .replaceAll('__PUBLIC_PATH__', getCDNURL()) + .replaceAll('__PUBLIC_PATH__', __webpack_public_path__.replace(/\/$/, '')) const minifiedHTML = minify(rawHTML, { collapseBooleanAttributes: true, collapseWhitespace: true, diff --git a/scripts/toolboxSrc/pushToCDN.ts b/scripts/toolboxSrc/pushToCDN.ts index e646fcc3c03..df6d52515df 100644 --- a/scripts/toolboxSrc/pushToCDN.ts +++ b/scripts/toolboxSrc/pushToCDN.ts @@ -25,46 +25,63 @@ const pushClientAssetsToCDN = async () => { console.log(`⛅️ Uploaded ${dirEnts.length} client assets to CDN`) } -const pushTemplatesToCDN = async () => { +const pushServerAssetsToCDN = async () => { const fileStoreManager = getFileStoreManager() - const collector = {} as Record - const context = (require as any).context( + const templatesContext = (require as any).context( '../../static/images/illustrations', false, /\/action.png$|\/teamPrompt.png$|Template.png$/ ) - - context.keys().forEach((relativePath: string) => { - const {name, ext} = path.parse(relativePath) - // This path only exists on the build machine - const builtPath = context(relativePath).default - // sub out the build machine path prefix with the __dirname - // e.g. /Users/CI/dist/templates/X.png -> /app/dist/templates/X.png - const absPath = builtPath.replace(/^.+\/dist(\/.+$)/, __dirname + '$1') - collector[`${name}${ext}`] = absPath + const templatePaths = new Set() + templatesContext.keys().forEach((relativePath: `./${string}`) => { + const {base} = path.parse(relativePath) + templatePaths.add(base) }) - const results = await Promise.all( - Object.entries(collector).map(async ([fileName, pathName]) => { - // store meeting templates under our Parabol ghost organization - const partialPath = `Organization/aGhostOrg/template/${fileName}` - const exists = await fileStoreManager.checkExists(partialPath) - if (exists) return false - const buffer = await fs.promises.readFile(pathName as string) - const {name, ext} = path.parse(fileName) - return fileStoreManager.putTemplateIllustration(buffer, 'aGhostOrg', ext, name) - }) - ) - const urls = results.filter(Boolean) + const isTemplate = (filename: string) => templatePaths.has(filename) + + const localServerAssetsDir = path.join(PROJECT_ROOT, 'dist', 'images') - if (urls.length > 0) { - console.log(urls.join('\n')) + // Use this pattern if this is a user asset (including aGhostUser) & kept in the DB + const templateFileUploader = async (filename: string) => { + const partialPath = `Organization/aGhostOrg/template/${filename}` + const exists = await fileStoreManager.checkExists(partialPath) + if (exists) return false + const buffer = await fs.promises.readFile(path.join(localServerAssetsDir, filename)) + const {name, ext} = path.parse(filename) + const url = await fileStoreManager.putTemplateIllustration(buffer, 'aGhostOrg', ext, name) + console.log(`⛅️ Uploaded template ${filename} to ${url}`) + return true } - console.log(`⛅️ Uploaded ${urls.length} Meeting Templates to CDN`) + // Use this pattern if the asset is publicly available + const defaultFileUploader = async (filename: string) => { + // static assets in /dist/images are already hosted at /static/images + if (process.env.FILE_STORE_PROVIDER === 'local') return + const targetObject = `images/${filename}` + const exists = await fileStoreManager.checkExists(targetObject) + if (exists) return false + const buffer = await fs.promises.readFile(path.join(localServerAssetsDir, filename)) + const url = await fileStoreManager.putBuildFile(buffer, targetObject) + console.log(`⛅️ Uploaded server asset ${targetObject} to ${url}`) + return true + } + + const dirEnts = await fs.promises.readdir(localServerAssetsDir, {withFileTypes: true}) + const entries = await Promise.all( + dirEnts.map(async (dirent) => { + const {name} = dirent + if (!dirent.isFile()) throw new Error(`⛅️ Expected ${name} to be a file`) + return isTemplate(name) ? templateFileUploader(name) : defaultFileUploader(name) + }) + ) + + const pushed = entries.filter(Boolean).length + console.log(`⛅️ Server upload complete. Pushed ${pushed} assets to CDN`) } + const pushToCDN = async () => { console.log('⛅️ Push to CDN Started') - await Promise.all([pushClientAssetsToCDN(), pushTemplatesToCDN()]) + await Promise.all([pushClientAssetsToCDN(), pushServerAssetsToCDN()]) console.log('⛅️ Push to CDN Complete') } diff --git a/scripts/webpack/dev.servers.config.js b/scripts/webpack/dev.servers.config.js index 80591f24b0f..9632b40f66f 100644 --- a/scripts/webpack/dev.servers.config.js +++ b/scripts/webpack/dev.servers.config.js @@ -10,6 +10,7 @@ const SERVER_ROOT = path.join(PROJECT_ROOT, 'packages', 'server') const EMBEDDER_ROOT = path.join(PROJECT_ROOT, 'packages', 'embedder') const GQL_ROOT = path.join(PROJECT_ROOT, 'packages', 'gql-executor') const DOTENV = path.join(PROJECT_ROOT, 'scripts', 'webpack', 'utils', 'dotenv.js') +const INIT_PUBLIC_PATH = path.join(SERVER_ROOT, 'initPublicPath.ts') // const CircularDependencyPlugin = require('circular-dependency-plugin') module.exports = { @@ -26,9 +27,9 @@ module.exports = { __dirname: false }, entry: { - web: [DOTENV, path.join(SERVER_ROOT, 'server.ts')], - embedder: [DOTENV, path.join(EMBEDDER_ROOT, 'embedder.ts')], - gqlExecutor: [DOTENV, path.join(GQL_ROOT, 'gqlExecutor.ts')] + web: [DOTENV, INIT_PUBLIC_PATH, path.join(SERVER_ROOT, 'server.ts')], + embedder: [DOTENV, INIT_PUBLIC_PATH, path.join(EMBEDDER_ROOT, 'embedder.ts')], + gqlExecutor: [DOTENV, INIT_PUBLIC_PATH, path.join(GQL_ROOT, 'gqlExecutor.ts')] }, output: { filename: '[name].js', @@ -81,7 +82,7 @@ module.exports = { { loader: 'file-loader', options: { - publicPath: `http://localhost:${process.env.PORT}/static/` + name: '[name].[ext]' } } ] diff --git a/scripts/webpack/prod.client.config.js b/scripts/webpack/prod.client.config.js index 080cf489c0c..4cc8788abcf 100644 --- a/scripts/webpack/prod.client.config.js +++ b/scripts/webpack/prod.client.config.js @@ -116,7 +116,7 @@ module.exports = (config) => { // Trying to keep GraphqlContainer out of here is difficult because there are a lot of common dependencies exclude: [/\.map$/, /^manifest.*\.js$/, /skeleton.html$/], modifyURLPrefix: { - '': '__PUBLIC_PATH__/' + '': '__PUBLIC_PATH__' } }), new MiniCssExtractPlugin({ diff --git a/scripts/webpack/prod.servers.config.js b/scripts/webpack/prod.servers.config.js index 52feb216089..4f11b012db4 100644 --- a/scripts/webpack/prod.servers.config.js +++ b/scripts/webpack/prod.servers.config.js @@ -15,6 +15,7 @@ const EMBEDDER_ROOT = path.join(PROJECT_ROOT, 'packages', 'embedder') const GQL_ROOT = path.join(PROJECT_ROOT, 'packages', 'gql-executor') const DOTENV = path.join(PROJECT_ROOT, 'scripts/webpack/utils/dotenv.js') const distPath = path.join(PROJECT_ROOT, 'dist') +const INIT_PUBLIC_PATH = path.join(SERVER_ROOT, 'initPublicPath.ts') const COMMIT_HASH = cp.execSync('git rev-parse HEAD').toString().trim() @@ -29,13 +30,18 @@ module.exports = (config) => { chronos: [DOTENV, path.join(PROJECT_ROOT, 'packages/chronos/chronos.ts')], web: [ DOTENV, + INIT_PUBLIC_PATH, // each instance of web needs to generate its own index.html to use on startup path.join(PROJECT_ROOT, 'scripts/toolboxSrc/applyEnvVarsToClientAssets.ts'), path.join(SERVER_ROOT, 'server.ts') ], embedder: [DOTENV, path.join(EMBEDDER_ROOT, 'embedder.ts')], - gqlExecutor: [DOTENV, path.join(GQL_ROOT, 'gqlExecutor.ts')], - preDeploy: [DOTENV, path.join(PROJECT_ROOT, 'scripts/toolboxSrc/preDeploy.ts')], + gqlExecutor: [DOTENV, INIT_PUBLIC_PATH, path.join(GQL_ROOT, 'gqlExecutor.ts')], + preDeploy: [ + DOTENV, + INIT_PUBLIC_PATH, + path.join(PROJECT_ROOT, 'scripts/toolboxSrc/preDeploy.ts') + ], pushToCDN: [DOTENV, path.join(PROJECT_ROOT, 'scripts/toolboxSrc/pushToCDN.ts')], migrate: [DOTENV, path.join(PROJECT_ROOT, 'scripts/toolboxSrc/standaloneMigrations.ts')], assignSURole: [DOTENV, path.join(PROJECT_ROOT, 'scripts/toolboxSrc/assignSURole.ts')] @@ -108,53 +114,18 @@ module.exports = (config) => { ...transformRules(PROJECT_ROOT, true), { test: /\.(png|jpg|jpeg|gif|svg)$/, - oneOf: [ - { - // Put templates in their own directory that will get pushed to the CDN. PG Migrations will reference that URL - test: /Template.png$/, - include: [path.resolve(PROJECT_ROOT, 'static/images/illustrations')], - use: [ - { - loader: 'file-loader', - options: { - name: 'templates/[name].[ext]', - publicPath: distPath - } - } - ] - }, - { - // manifest.json icons just need the file name, we'll prefix them with the CDN in preDeploy - test: /mark-cropped-\d+.png$/, - include: [path.resolve(PROJECT_ROOT, 'static/images/brand')], - use: [ - { - loader: 'file-loader', - options: { - name: '[name].[ext]' - } - } - ] - }, - { - use: [ - { - loader: 'file-loader', - options: { - publicPath: distPath, - name: '[name].[ext]' - } - } - ] - } - ] + type: 'asset/resource', + generator: { + filename: 'images/[name][ext]' + } }, { include: [/node_modules/], test: /\.node$/, use: [ { - loader: 'node-loader', + // use our fork of node-loader to exclude the public path from the script + loader: path.resolve(__dirname, './utils/node-loader-private/cjs.js'), options: { // sharp's bindings.gyp is hardcoded to look for libvips 2 directories up // rather than do a custom build, we just output it 2 directories down (/node/binaries) diff --git a/scripts/webpack/utils/node-loader-private/cjs.js b/scripts/webpack/utils/node-loader-private/cjs.js new file mode 100644 index 00000000000..9ad6efe69c2 --- /dev/null +++ b/scripts/webpack/utils/node-loader-private/cjs.js @@ -0,0 +1,6 @@ +"use strict"; + +const loader = require("./index"); + +module.exports = loader.default; +module.exports.raw = loader.raw; \ No newline at end of file diff --git a/scripts/webpack/utils/node-loader-private/index.js b/scripts/webpack/utils/node-loader-private/index.js new file mode 100644 index 00000000000..1bdea848355 --- /dev/null +++ b/scripts/webpack/utils/node-loader-private/index.js @@ -0,0 +1,36 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = loader; +exports.raw = void 0; + +var _loaderUtils = require("loader-utils"); + +var _options = _interopRequireDefault(require("./options.json")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ +function loader(content) { + const options = this.getOptions(_options.default); + const name = (0, _loaderUtils.interpolateName)(this, typeof options.name !== "undefined" ? options.name : "[contenthash].[ext]", { + context: this.rootContext, + content + }); + this.emitFile(name, content); + return ` +try { + process.dlopen(module, __dirname + require("path").sep + ${JSON.stringify(name)}${typeof options.flags !== "undefined" ? `, ${JSON.stringify(options.flags)}` : ""}); +} catch (error) { + throw new Error('node-loader:\\n' + error); +} +`; +} + +const raw = true; +exports.raw = raw; diff --git a/scripts/webpack/utils/node-loader-private/options.json b/scripts/webpack/utils/node-loader-private/options.json new file mode 100644 index 00000000000..d8322360ca9 --- /dev/null +++ b/scripts/webpack/utils/node-loader-private/options.json @@ -0,0 +1,20 @@ +{ + "title": "Node Loader options", + "type": "object", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "instanceof": "Function" + } + ] + }, + "flags": { + "type": "integer" + } + }, + "additionalProperties": false +} diff --git a/yarn.lock b/yarn.lock index 17637e0e348..74d28b2fb23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2829,6 +2829,13 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@datadog/browser-core@3.6.12": version "3.6.12" resolved "https://registry.yarnpkg.com/@datadog/browser-core/-/browser-core-3.6.12.tgz#6fcdbd6809656544289f7684f03b88a598cc3345" @@ -3196,6 +3203,13 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.0.0.tgz#f22824caff3ae506b18207bad4126dbc6ccdb6b8" integrity sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ== +"@fastify/merge-json-schemas@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz#3551857b8a17a24e8c799e9f51795edb07baa0bc" + integrity sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA== + dependencies: + fast-deep-equal "^3.1.3" + "@floating-ui/core@^1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.3.1.tgz#4d795b649cc3b1cbb760d191c80dcb4353c9a366" @@ -4087,6 +4101,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" @@ -4105,6 +4124,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.8", "@jridgewell/trace-mapping@^0.3.9": version "0.3.18" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" @@ -5677,6 +5704,19 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-alert-dialog@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz#70dd529cbf1e4bff386814d3776901fcaa131b8c" + integrity sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dialog" "1.0.5" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-arrow@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d" @@ -5685,6 +5725,32 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" +"@radix-ui/react-avatar@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623" + integrity sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + +"@radix-ui/react-collapsible@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz#df0e22e7a025439f13f62d4e4a9e92c4a0df5b81" + integrity sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-collection@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.2.tgz#d50da00bfa2ac14585319efdbbb081d4c5a29a97" @@ -5735,6 +5801,27 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dialog@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" + integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.4" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-controllable-state" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + "@radix-ui/react-dialog@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.4.tgz#06bce6c16bb93eb36d7a8589e665a20f4c1c52c1" @@ -5825,6 +5912,16 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-focus-scope@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525" + integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-id@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.0.tgz#8d43224910741870a45a8c9d092f25887bb6d11e" @@ -7259,6 +7356,26 @@ mkdirp "^1.0.4" path-browserify "^1.0.1" +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@types/analytics-node@^3.1.3": version "3.1.7" resolved "https://registry.yarnpkg.com/@types/analytics-node/-/analytics-node-3.1.7.tgz#cb97c80ee505094e44a0188c3ad25f70c67e3c65" @@ -8458,6 +8575,11 @@ acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.2.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn-walk@^8.1.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + acorn@^7.0.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" @@ -8468,6 +8590,11 @@ acorn@^8.0.4, acorn@^8.1.0, acorn@^8.7.0, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8 resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +acorn@^8.4.1: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + adaptivecards@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/adaptivecards/-/adaptivecards-2.10.0.tgz#1c94e84491afe5a4f2d4060f6e0fc57b895205e3" @@ -8545,7 +8672,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -8555,7 +8682,7 @@ ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.12.0, ajv@^8.6.0, ajv@^8.8.0: +ajv@^8.0.0, ajv@^8.10.0, ajv@^8.12.0, ajv@^8.6.0, ajv@^8.8.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -9577,9 +9704,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001580, caniuse-lite@~1.0.0: - version "1.0.30001571" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001571.tgz#4182e93d696ff42930f4af7eba515ddeb57917ac" - integrity sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ== + version "1.0.30001594" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001594.tgz#bea552414cd52c2d0c985ed9206314a696e685f5" + integrity sha512-VblSX6nYqyJVs8DKFMldE2IVCJjZ225LW00ydtUWwh5hk9IfkTOffO6r8gJNsH0qqqeAF8KrbMYA2VEwTlGW5g== capital-case@^1.0.4: version "1.0.4" @@ -11173,6 +11300,7 @@ draft-js-utils@^1.4.0: "draft-js@https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6": version "0.10.5" + uid "025fddba56f21aaf3383aee778e0b17025c9a7bc" resolved "https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6#025fddba56f21aaf3383aee778e0b17025c9a7bc" dependencies: fbjs "^0.8.15" @@ -11910,14 +12038,18 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-sta resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-json-stringify@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-1.21.0.tgz#51bc8c6d77d8c7b2cc7e5fa754f7f909f9e1262f" - integrity sha512-xY6gyjmHN3AK1Y15BCbMpeO9+dea5ePVsp3BouHCdukcx0hOHbXwFhRodhcI0NpZIgDChSeAKkHW9YjKvhwKBA== +fast-json-stringify@^5.7.0: + version "5.12.0" + resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-5.12.0.tgz#e9f77dc0b4face74351320c3618f1d869de5cb18" + integrity sha512-7Nnm9UPa7SfHRbHVA1kJQrGXCRzB7LMlAAqHXQFkEQqueJm1V8owm0FsE/2Do55/4CcdhwiLQERaKomOnKQkyA== dependencies: - ajv "^6.11.0" - deepmerge "^4.2.2" - string-similarity "^4.0.1" + "@fastify/merge-json-schemas" "^0.1.0" + ajv "^8.10.0" + ajv-formats "^2.1.1" + fast-deep-equal "^3.1.3" + fast-uri "^2.1.0" + json-schema-ref-resolver "^1.0.1" + rfdc "^1.2.0" fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" @@ -11941,6 +12073,11 @@ fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== +fast-uri@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-2.3.0.tgz#bdae493942483d299e7285dcb4627767d42e2793" + integrity sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw== + fast-url-parser@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" @@ -12844,15 +12981,14 @@ graphql-executor@0.0.22: resolved "https://registry.yarnpkg.com/graphql-executor/-/graphql-executor-0.0.22.tgz#14bc466bb27ab38346998e0b375cba55685eed94" integrity sha512-WbKSnSHFn6REKKH4T6UAwDM3mLUnYMQlQLNG0Fw+Lkb3ilCnL3m5lkJ7411LAI9sF7BvPbthovVZhsEUh9Xfag== -graphql-jit@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/graphql-jit/-/graphql-jit-0.7.4.tgz#bc8ccf79596d13dff3835902a466f9a5ecc3a8c1" - integrity sha512-kWyHmsQtKMD6xcKDgf4dgPLyIZhviqA6IWGdnA0ElL9wgrIOTxf3eI4c0/U3tnoAU3t09zliVCfDkfIptzYjIA== +graphql-jit@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/graphql-jit/-/graphql-jit-0.8.4.tgz#53c2e43b90ec98ea0942f4062516de910fbff709" + integrity sha512-4KRrJ1ROy3Usgbl3eAoUMfdfZCRjkcw9cCGT7QwTUIHm9dPGaSaldxzGUttyjErU0rsYEb6WWyb6mMh5r6lEoQ== dependencies: - "@graphql-typed-document-node/core" "^3.1.1" - fast-json-stringify "^1.21.0" + "@graphql-typed-document-node/core" "^3.2.0" + fast-json-stringify "^5.7.0" generate-function "^2.3.1" - json-schema "^0.4.0" lodash.memoize "^4.1.2" lodash.merge "4.6.2" lodash.mergewith "4.6.2" @@ -14629,6 +14765,13 @@ json-schema-merge-allof@^0.8.1: json-schema-compare "^0.2.2" lodash "^4.17.20" +json-schema-ref-resolver@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz#6586f483b76254784fc1d2120f717bdc9f0a99bf" + integrity sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw== + dependencies: + fast-deep-equal "^3.1.3" + json-schema-to-ts@^2.6.2-beta.0: version "2.8.2" resolved "https://registry.yarnpkg.com/json-schema-to-ts/-/json-schema-to-ts-2.8.2.tgz#233b810b73f01e0ab93ad06ddccb1c2b98f23b8d" @@ -16793,10 +16936,10 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -oy-vey@^0.11.0: - version "0.11.2" - resolved "https://registry.yarnpkg.com/oy-vey/-/oy-vey-0.11.2.tgz#3bbc36a4064993a2ff52f985b066d44dcb4500d9" - integrity sha512-06prDST4MicbAWie4eXcouJbGhAu0r7j3Yta1KFtgs7v2t7goHmY06/GWFjT6lpIsGKJC+7vZtwdecRSYnFtPQ== +oy-vey@^0.12.1: + version "0.12.1" + resolved "https://registry.yarnpkg.com/oy-vey/-/oy-vey-0.12.1.tgz#a3bdda93bdb3a9f483fc01432b7b8d5abf838405" + integrity sha512-fFpS8mRoXqMTPYXUoDTO6S5S8WXvqBE+AOPbDwxFu2n3ZNns6x3+Ml/49lAomyJV6RzdFBVA5LxCUEHNaLlhjA== dependencies: clean-css "^4.0.12" object-assign "^4.1.1" @@ -18924,6 +19067,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" + integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== + rfdc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" @@ -19847,11 +19995,6 @@ string-similarity@^3.0.0: resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-3.0.0.tgz#07b0bc69fae200ad88ceef4983878d03793847c7" integrity sha512-7kS7LyTp56OqOI2BDWQNVnLX/rCxIQn+/5M0op1WV6P8Xx6TZNdajpuqQdiJ7Xx+p1C5CsWMvdiBp9ApMhxzEQ== -string-similarity@^4.0.1: - version "4.0.4" - resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" - integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== - "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -20623,15 +20766,23 @@ ts-node-dev@^1.0.0-pre.44: ts-node "^9.0.0" tsconfig "^7.0.0" -ts-node@^8.6.2: - version "8.10.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" - integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== - dependencies: +ts-node@10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" arg "^4.1.0" + create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" - source-map-support "^0.5.17" + v8-compile-cache-lib "^3.0.1" yn "3.1.1" ts-node@^9.0.0: @@ -21144,6 +21295,11 @@ uuid@^9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@2.3.0, v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" From 497ba4b0311d2e825d74f1190dc76280a8e1ab0c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:29:49 +0100 Subject: [PATCH 07/19] chore(release): Test v7.22.3 (#9550) Signed-off-by: dependabot[bot] Signed-off-by: Matt Krick Co-authored-by: Rafa <101704572+rafaelromcar-parabol@users.noreply.github.com> Co-authored-by: Georg Bremer Co-authored-by: Nick O'Ferrall Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> Co-authored-by: Matt Krick Co-authored-by: Jordan Husney Co-authored-by: adaniels-parabol <71724289+adaniels-parabol@users.noreply.github.com> Co-authored-by: snyk-bot Co-authored-by: Dale Bumblis <135627447+dbumblis-parabol@users.noreply.github.com> Co-authored-by: Bartosz Jarocki Co-authored-by: Marcus Wermuth Co-authored-by: Rafael Romero Co-authored-by: github-actions --- .env.example | 15 +- .github/workflows/README.md | 2 +- .github/workflows/build.yml | 7 +- .github/workflows/test.yml | 2 +- .release-please-manifest.json | 2 +- CHANGELOG.md | 16 ++ docker-compose.yml | 53 ---- docker/Dockerfile.prod | 41 --- docker/README.md | 19 -- docker/docker-compose.selfHosted.yml | 6 - docker/entrypoint.prod.sh | 4 - .../parabol-ubi}/.gitignore | 0 .../parabol-ubi}/README.md | 8 +- .../parabol-ubi}/cloudbuild.yaml | 0 .../parabol-ubi}/docker-compose.yml | 0 .../parabol-ubi}/dockerfiles/basic.dockerfile | 4 +- .../dockerfiles/parabol.dockerfile | 0 .../entrypoints/docker-entrypoint.sh | 0 .../parabol-ubi}/environments/basic-env | 2 +- .../parabol-ubi}/environments/legacy-build | 0 .../parabol-ubi}/environments/pipeline | 2 +- ...rule_account_disable_post_pw_expiration.sh | 0 ....content_rule_accounts_logon_fail_delay.sh | 0 ..._accounts_max_concurrent_login_sessions.sh | 0 ...nt_rule_accounts_maximum_age_login_defs.sh | 0 ...nt_rule_accounts_minimum_age_login_defs.sh | 0 ...e_accounts_password_all_shadowed_sha512.sh | 0 ...ule_accounts_password_minlen_login_defs.sh | 0 ...tent_rule_accounts_password_pam_dcredit.sh | 0 ...nt_rule_accounts_password_pam_dictcheck.sh | 0 ...ontent_rule_accounts_password_pam_difok.sh | 0 ...tent_rule_accounts_password_pam_lcredit.sh | 0 ...le_accounts_password_pam_maxclassrepeat.sh | 0 ...nt_rule_accounts_password_pam_maxrepeat.sh | 0 ...ent_rule_accounts_password_pam_minclass.sh | 0 ...ntent_rule_accounts_password_pam_minlen.sh | 0 ...tent_rule_accounts_password_pam_ocredit.sh | 0 ...rd_pam_pwhistory_remember_password_auth.sh | 0 ...word_pam_pwhistory_remember_system_auth.sh | 0 ...tent_rule_accounts_password_pam_ucredit.sh | 0 ...ule_accounts_password_pam_unix_remember.sh | 0 ...nt_rule_accounts_passwords_pam_faillock.sh | 0 ...le_accounts_passwords_pam_faillock_deny.sh | 0 ...counts_passwords_pam_faillock_deny_root.sh | 0 ...ccounts_passwords_pam_faillock_interval.sh | 0 ...unts_passwords_pam_faillock_unlock_time.sh | 0 ....content_rule_accounts_umask_etc_bashrc.sh | 0 ...ntent_rule_accounts_umask_etc_csh_cshrc.sh | 0 ...tent_rule_accounts_umask_etc_login_defs.sh | 0 ...content_rule_accounts_umask_etc_profile.sh | 0 ...sgproject.content_rule_banner_etc_issue.sh | 0 ...ct.content_rule_configure_crypto_policy.sh | 0 ...t_rule_configure_kerberos_crypto_policy.sh | 0 ...nt_rule_configure_openssl_crypto_policy.sh | 0 ...nt_rule_configure_usbguard_auditbackend.sh | 0 ...ontent_rule_coredump_disable_backtraces.sh | 0 ...t.content_rule_coredump_disable_storage.sh | 0 ...ent_rule_disable_ctrlaltdel_burstaction.sh | 0 ...ct.content_rule_disable_users_coredumps.sh | 0 ...ect.content_rule_display_login_attempts.sh | 0 ...ent_rule_ensure_gpgcheck_local_packages.sh | 0 ...t_rule_file_groupowner_var_log_messages.sh | 0 ...ile_groupownership_system_commands_dirs.sh | 0 ...ontent_rule_file_owner_var_log_messages.sh | 0 ...content_rule_kernel_module_atm_disabled.sh | 0 ...content_rule_kernel_module_can_disabled.sh | 0 ...tent_rule_kernel_module_cramfs_disabled.sh | 0 ...le_kernel_module_firewire-core_disabled.sh | 0 ...ontent_rule_kernel_module_sctp_disabled.sh | 0 ...ontent_rule_kernel_module_tipc_disabled.sh | 0 ...project.content_rule_no_empty_passwords.sh | 0 ...content_rule_openssl_use_strong_entropy.sh | 0 ..._rule_package_crypto-policies_installed.sh | 0 ...content_rule_package_iptables_installed.sh | 0 ...ontent_rule_package_rng-tools_installed.sh | 0 ...ect.content_rule_package_sudo_installed.sh | 0 ...content_rule_package_usbguard_installed.sh | 0 ...tent_rule_sudo_require_reauthentication.sh | 0 ...ct.content_rule_sudoers_validate_passwd.sh | 0 .../tools/ip-to-server_id/index.js | 0 .../tools/ip-to-server_id/package.json | 0 .../images}/postgres/Dockerfile | 8 +- docker/images/postgres/extensions/install.sql | 1 + docker/stacks/development/README.md | 43 +++ .../datadog}/dd-conf.d/gqlExecutor.yml | 0 .../development/datadog}/dd-conf.d/web.yml | 0 .../development/docker-compose.yml} | 26 +- .../stacks/development/redis/certs}/README.md | 10 +- .../redis/certs}/gen-redis-certs.sh | 0 .../stacks/development/redis/certs}/redis.crt | 0 .../stacks/development/redis/certs}/redis.key | 0 .../development/redis/certs}/redisCA.crt | 0 .../single-tenant-host}/.env.example | 0 .../single-tenant-host}/README.md | 10 +- .../single-tenant-host}/docker-compose.yaml | 52 ++-- .../us-department-of-defense/README.md | 2 +- package.json | 6 +- packages/chronos/package.json | 4 +- .../ActivityLibrary/ActivityCard.tsx | 2 +- packages/client/package.json | 2 +- packages/client/utils/makeDefaultTeamName.ts | 54 ---- packages/gql-executor/package.json | 6 +- packages/integration-tests/package.json | 2 +- packages/server/database/README.md | 1 - .../mutations/helpers/bootstrapNewUser.ts | 3 +- .../graphql/mutations/updateTeamName.ts | 9 +- packages/server/package.json | 4 +- packages/server/postgres/README.md | 23 -- .../server/postgres/extensions/install.sql | 2 - .../extensions/postgres-json-schema/Makefile | 7 - .../postgres-json-schema--0.1.1.sql | 259 ------------------ .../postgres-json-schema.control | 3 - packages/server/postgres/postgres.conf | 1 - packages/server/utils/uwsGetIP.ts | 6 +- 114 files changed, 158 insertions(+), 571 deletions(-) delete mode 100644 docker-compose.yml delete mode 100644 docker/Dockerfile.prod delete mode 100644 docker/README.md delete mode 100644 docker/docker-compose.selfHosted.yml delete mode 100644 docker/entrypoint.prod.sh rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/.gitignore (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/README.md (95%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/cloudbuild.yaml (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/docker-compose.yml (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/dockerfiles/basic.dockerfile (75%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/dockerfiles/parabol.dockerfile (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/entrypoints/docker-entrypoint.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/environments/basic-env (81%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/environments/legacy-build (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/environments/pipeline (93%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_account_disable_post_pw_expiration.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_logon_fail_delay.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_max_concurrent_login_sessions.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_maximum_age_login_defs.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_minimum_age_login_defs.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_all_shadowed_sha512.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_minlen_login_defs.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dcredit.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dictcheck.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_difok.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_lcredit.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxclassrepeat.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxrepeat.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minclass.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minlen.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ocredit.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_password_auth.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_system_auth.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ucredit.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_unix_remember.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny_root.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_interval.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_unlock_time.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_bashrc.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_csh_cshrc.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_login_defs.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_profile.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_banner_etc_issue.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_configure_crypto_policy.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_configure_kerberos_crypto_policy.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_configure_openssl_crypto_policy.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_configure_usbguard_auditbackend.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_coredump_disable_backtraces.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_coredump_disable_storage.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_burstaction.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_disable_users_coredumps.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_display_login_attempts.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_ensure_gpgcheck_local_packages.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_file_groupowner_var_log_messages.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_file_groupownership_system_commands_dirs.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_file_owner_var_log_messages.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_kernel_module_atm_disabled.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_kernel_module_can_disabled.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_kernel_module_cramfs_disabled.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_kernel_module_firewire-core_disabled.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_kernel_module_sctp_disabled.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_kernel_module_tipc_disabled.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_no_empty_passwords.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_openssl_use_strong_entropy.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_package_crypto-policies_installed.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_package_iptables_installed.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_package_rng-tools_installed.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_package_sudo_installed.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_package_usbguard_installed.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_sudo_require_reauthentication.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/security/xccdf_org.ssgproject.content_rule_sudoers_validate_passwd.sh (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/tools/ip-to-server_id/index.js (100%) rename docker/{parabol-ubi/docker-build => images/parabol-ubi}/tools/ip-to-server_id/package.json (100%) rename {packages/server => docker/images}/postgres/Dockerfile (72%) create mode 100644 docker/images/postgres/extensions/install.sql create mode 100644 docker/stacks/development/README.md rename docker/{ => stacks/development/datadog}/dd-conf.d/gqlExecutor.yml (100%) rename docker/{ => stacks/development/datadog}/dd-conf.d/web.yml (100%) rename docker/{dev.yml => stacks/development/docker-compose.yml} (76%) rename {certs => docker/stacks/development/redis/certs}/README.md (74%) rename {certs => docker/stacks/development/redis/certs}/gen-redis-certs.sh (100%) rename {certs => docker/stacks/development/redis/certs}/redis.crt (100%) rename {certs => docker/stacks/development/redis/certs}/redis.key (100%) rename {certs => docker/stacks/development/redis/certs}/redisCA.crt (100%) rename docker/{parabol-ubi/docker-host-st => stacks/single-tenant-host}/.env.example (100%) rename docker/{parabol-ubi/docker-host-st => stacks/single-tenant-host}/README.md (88%) rename docker/{parabol-ubi/docker-host-st => stacks/single-tenant-host}/docker-compose.yaml (83%) delete mode 100644 packages/client/utils/makeDefaultTeamName.ts delete mode 100644 packages/server/postgres/extensions/install.sql delete mode 100644 packages/server/postgres/extensions/postgres-json-schema/Makefile delete mode 100644 packages/server/postgres/extensions/postgres-json-schema/postgres-json-schema--0.1.1.sql delete mode 100644 packages/server/postgres/extensions/postgres-json-schema/postgres-json-schema.control delete mode 100644 packages/server/postgres/postgres.conf diff --git a/.env.example b/.env.example index 0d0a44b41f0..2aa11780748 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,9 @@ PROTO='http' SERVER_SECRET='key_SERVER_SECRET' # Cluster node number 0 - 1023. Must be unique per process. SERVER_ID='1' +# Used to read the client IP from the X-Forwarded-For header, if not set, it will use the first IP in the list. +# If configured, it must match the number of proxies in the stack, otherwise it might rate limit all traffic coming from the proxy. +# TRUSTED_PROXY_COUNT='1' # Websocket port for the websocket server, only used in development (yarn dev) SOCKET_PORT='3001' @@ -74,9 +77,9 @@ POSTGRES_USE_PGVECTOR=true # POSTGRES_SSL_DIR='/var/lib/postgresql' REDIS_PASSWORD='' REDIS_URL='redis://localhost:6379' -# REDIS_TLS_CERT_FILE=./certs/redis.crt -# REDIS_TLS_KEY_FILE=./certs/redis.key -# REDIS_TLS_CA_FILE=./certs/redisCA.crt +# REDIS_TLS_CERT_FILE=./docker/stacks/development/redis/certs/redis.crt +# REDIS_TLS_KEY_FILE=./docker/stacks/development/redis/certs/redis.key +# REDIS_TLS_CA_FILE=./docker/stacks/development/redis/certs/redisCA.crt # REDIS_TLS_REJECT_UNAUTHORIZED='false' RETHINKDB_URL='rethinkdb://localhost:28015/actionDevelopment' RETHINKDB_SSL='false' @@ -119,9 +122,9 @@ RETHINKDB_SSL='false' # RECALL_AI_KEY='' # SLACK_CLIENT_ID='key_SLACK_CLIENT_ID' # SLACK_CLIENT_SECRET='key_SLACK_CLIENT_SECRET' -# STRIPE_SECRET_KEY='sk_test_4eC39HqLyjWDarjtT1zdp7dc' -# STRIPE_PUBLISHABLE_KEY='pk_test_TYooMQauvdEDq54NiTphI7jx' -# STRIPE_WEBHOOK_SECRET='sk_test_4eC39HqLyjWDarjtT1zdp7dc' +# STRIPE_SECRET_KEY='' +# STRIPE_PUBLISHABLE_KEY='' +# STRIPE_WEBHOOK_SECRET='' # MAIL # MAIL GLOBALS. PROVIDER: mailgun | google | debug | smtp diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 244f4efb0da..0076af84bab 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -2,7 +2,7 @@ ## Runners -To run `docker-build.yml`, GitHub requires using a larger runner. +To run `build.yml`, GitHub requires using a larger runner. This is because we're webpackifying all the code in node_modules into a single `.js.`. Doing all that transpiling can take a LOT of memory. 8GB+. At this time, large GitHub-hosted runners are in beta. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a19c552ac06..709f0a2d818 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,8 +12,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true env: - PARABOL_DOCKERFILE: ./docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile - PARABOL_BUILD_ENV_PATH: docker/parabol-ubi/docker-build/environments/pipeline + PARABOL_DOCKERFILE: ./docker/images/parabol-ubi/dockerfiles/basic.dockerfile + PARABOL_BUILD_ENV_PATH: docker/images/parabol-ubi/environments/pipeline jobs: build: runs-on: ubuntu-8cores @@ -22,8 +22,7 @@ jobs: id-token: "write" services: postgres: - # Image is pinned to v15, OK since it's just for testing - image: ankane/pgvector + image: pgvector/pgvector:pg15 # This env variables must be the same in the file PARABOL_BUILD_ENV_PATH env: POSTGRES_PASSWORD: "temppassword" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb6c94bd9a4..8378696b622 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: - PARABOL_BUILD_ENV_PATH: docker/parabol-ubi/docker-build/environments/pipeline + PARABOL_BUILD_ENV_PATH: docker/images/parabol-ubi/environments/pipeline jobs: test: runs-on: ubuntu-8cores diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a5e4e701ea7..f9343c64067 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.22.2" + ".": "7.22.3" } diff --git a/CHANGELOG.md b/CHANGELOG.md index e40f96936c3..b4b60c13d98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.22.3](https://github.com/ParabolInc/parabol/compare/v7.22.2...v7.22.3) (2024-03-19) + + +### Fixed + +* Activity library illustrations in Firefox ([#9549](https://github.com/ParabolInc/parabol/issues/9549)) ([00a1ca2](https://github.com/ParabolInc/parabol/commit/00a1ca2977cd1117b030aa538f526a24ca395ac9)) +* **build-ci:** docker-build-push action fixed ([f16c21f](https://github.com/ParabolInc/parabol/commit/f16c21ffcfd99f98b49846951e561c4afeebbdf2)) +* Configure trusted proxies ([#9548](https://github.com/ParabolInc/parabol/issues/9548)) ([24df17b](https://github.com/ParabolInc/parabol/commit/24df17bf3f0979ab65f785e95711ba53158ecb42)) +* **parabol-ubi:** references to local files corrected ([41f5654](https://github.com/ParabolInc/parabol/commit/41f5654bc3046f770893c6840d4843ff58bce087)) + + +### Changed + +* Remove random team names ([#9543](https://github.com/ParabolInc/parabol/issues/9543)) ([fe128f0](https://github.com/ParabolInc/parabol/commit/fe128f017f01148ebd132fd532a771c6ab80ef16)) +* **repo-structure:** Docker images and stacks organized and clarified ([#9530](https://github.com/ParabolInc/parabol/issues/9530)) ([6fca12c](https://github.com/ParabolInc/parabol/commit/6fca12c814f471ef33954381ee562cbbb4b93d67)) + ## [7.22.2](https://github.com/ParabolInc/parabol/compare/v7.22.1...v7.22.2) (2024-03-18) diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 941a0178a80..00000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,53 +0,0 @@ -version: "3.7" - -services: - db: - image: rethinkdb:2.4.2 - ports: - - "8080:8080" - - "29015:29015" - - "28015:28015" - volumes: - - rethink-data:/data - networks: - - parabol-network - postgres: - image: postgres:15.4 - restart: always - env_file: .env - ports: - - "5432:5432" - volumes: - - "./packages/server/postgres/postgres.conf:/usr/local/etc/postgres/postgres.conf" - - "postgres-data:/data" - command: "postgres -c config_file=/usr/local/etc/postgres/postgres.conf" - networks: - - parabol-network - redis: - image: redis:7.0-alpine - ports: - - "6379:6379" - volumes: - - redis-data:/data - networks: - - parabol-network - app: - build: - context: . - dockerfile: ./docker/Dockerfile.prod - target: prod - env_file: .env - ports: - - "3000:3000" - depends_on: - - db - - redis - - postgres - networks: - - parabol-network -networks: - parabol-network: -volumes: - redis-data: {} - rethink-data: {} - postgres-data: {} diff --git a/docker/Dockerfile.prod b/docker/Dockerfile.prod deleted file mode 100644 index c070ef1e02d..00000000000 --- a/docker/Dockerfile.prod +++ /dev/null @@ -1,41 +0,0 @@ -# First, install all dependencies, including devDependencies, to run the build process -FROM node:20.11.0 as build - -WORKDIR /parabol - -# Only copy dependency-related files here (vs. COPY . .) to avoid breaking the cache and running -# the slow `yarn install` more than necessary -COPY package.json yarn.lock ./ -COPY packages/client/package.json ./packages/client/package.json -COPY packages/gql-executor/package.json packages/gql-executor/package.json -COPY packages/integration-tests/package.json packages/integration-tests/package.json -COPY packages/server/package.json packages/server/package.json -RUN yarn install --frozen-lockfile && \ - yarn cache clean - -COPY . . -RUN yarn build - -# Now, start over with a new stage so we don't pull over devDependencies -FROM node:20.11.0 as prod - -WORKDIR /parabol - -COPY package.json yarn.lock ./ -COPY packages/client/package.json ./packages/client/package.json -COPY packages/gql-executor/package.json packages/gql-executor/package.json -COPY packages/integration-tests/package.json packages/integration-tests/package.json -COPY packages/server/package.json packages/server/package.json -# Only install production dependencies -RUN yarn install --prod --frozen-lockfile && \ - yarn cache clean - -COPY . . -COPY --from=build /parabol/build ./build -COPY --from=build /parabol/dist ./dist - -RUN cp docker/entrypoint.prod.sh /bin/entrypoint && \ - chmod +x /bin/entrypoint -EXPOSE 80 -ENTRYPOINT [ "entrypoint" ] -CMD ["yarn", "start"] diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index b2ab1cefb56..00000000000 --- a/docker/README.md +++ /dev/null @@ -1,19 +0,0 @@ -## Usage of Docker in Different Envs - -### Development - -In development, docker handles all db services for us. This is done by calling `docker-compose` on the Compose file `./docker/dev.yml`. The web app itself is not containerized; it simply runs on the host machine by calling `yarn && yarn dev` - -### Production - -In production, dokku creates a Docker container using the [default Node.js heroku buildpack](https://dokku.com/docs~v0.5.1/deployment/buildpacks/). While it's possible to do so, we don't yet provide any [custom Dockerfile to dokku](https://dokku.com/docs~v0.5.1/deployment/dockerfiles/). - -### Self-Hosted - -Self-hosted instances may use the `docker-compose.yml` file in the root of the project directory. All services, including databases and the web app, will be containerized. - -If the owner of the self-hosted instance wants to use local file storage (instead of cloud storage such as AWS or GCP) for user uploaded images, make sure `FILE_STORE_PROVIDER='local'` in the root `.env` file. Additionally, the app must be deployed using the following command: - -`docker-compose -f docker-compose.yml -f ./docker/docker-compose.selfHosted.yml up -d` - -This ensures that the images will be persisted in a Docker volume. diff --git a/docker/docker-compose.selfHosted.yml b/docker/docker-compose.selfHosted.yml deleted file mode 100644 index 1c6598e42a3..00000000000 --- a/docker/docker-compose.selfHosted.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - app: - volumes: - - app-data:/parabol/self-hosted -volumes: - app-data: {} diff --git a/docker/entrypoint.prod.sh b/docker/entrypoint.prod.sh deleted file mode 100644 index 4ce89f9bce8..00000000000 --- a/docker/entrypoint.prod.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -yarn predeploy -exec "$@" diff --git a/docker/parabol-ubi/docker-build/.gitignore b/docker/images/parabol-ubi/.gitignore similarity index 100% rename from docker/parabol-ubi/docker-build/.gitignore rename to docker/images/parabol-ubi/.gitignore diff --git a/docker/parabol-ubi/docker-build/README.md b/docker/images/parabol-ubi/README.md similarity index 95% rename from docker/parabol-ubi/docker-build/README.md rename to docker/images/parabol-ubi/README.md index 85c1a2d42b3..920c6d48fd8 100644 --- a/docker/parabol-ubi/docker-build/README.md +++ b/docker/images/parabol-ubi/README.md @@ -21,9 +21,9 @@ Recommended: | `postgresql_tag` | PostgreSQL version from the [Docker image](https://hub.docker.com/_/postgres) | `Any tag` | `15.4` | | `rethinkdb_tag` | RethinkDB version from the [Docker image](https://hub.docker.com/_/rethinkdb) | `Any tag` | `2.4.2` | | `redis_tag` | Redis version from the [Docker image](https://hub.docker.com/_/redis) | `Any tag` | `7.0-alpine` | -| `_BUILD_ENV_PATH` | File `.env` used by the application during the build process | `Relative path from the root level of the repository` | `docker/parabol-ubi/docker-build/environments/basic-env` | +| `_BUILD_ENV_PATH` | File `.env` used by the application during the build process | `Relative path from the root level of the repository` | `docker/parabol-ubi/environments/basic-env` | | `_NODE_VERSION` | Node version, used by Docker to use the Docker image node:\_NODE_VERSION as base image to build | `Same as in root package.json` | | -| `_DOCKERFILE` | Dockerfile used to build the image | `Relative path from the root level of the repository` | `./docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile` | +| `_DOCKERFILE` | Dockerfile used to build the image | `Relative path from the root level of the repository` | `./docker/parabol-ubi/dockerfiles/basic.dockerfile` | | `_DOCKER_REPOSITORY` | The destination repository | `String` | `parabol` | | `_DOCKER_TAG` | Tag for the produced image | `String` | | @@ -33,9 +33,9 @@ Example of variables: export postgresql_tag=15.4; \ export rethinkdb_tag=2.4.2; \ export redis_tag=7.0-alpine; \ -export _BUILD_ENV_PATH=docker/parabol-ubi/docker-build/environments/basic-env; \ +export _BUILD_ENV_PATH=docker/parabol-ubi/environments/basic-env; \ export _NODE_VERSION=$(jq -r -j '.engines.node|ltrimstr("^")' package.json); \ -export _DOCKERFILE=./docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile; \ +export _DOCKERFILE=./docker/parabol-ubi/dockerfiles/basic.dockerfile; \ export _DOCKER_REPOSITORY=parabol; \ export _DOCKER_TAG=test-image ``` diff --git a/docker/parabol-ubi/docker-build/cloudbuild.yaml b/docker/images/parabol-ubi/cloudbuild.yaml similarity index 100% rename from docker/parabol-ubi/docker-build/cloudbuild.yaml rename to docker/images/parabol-ubi/cloudbuild.yaml diff --git a/docker/parabol-ubi/docker-build/docker-compose.yml b/docker/images/parabol-ubi/docker-compose.yml similarity index 100% rename from docker/parabol-ubi/docker-build/docker-compose.yml rename to docker/images/parabol-ubi/docker-compose.yml diff --git a/docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile b/docker/images/parabol-ubi/dockerfiles/basic.dockerfile similarity index 75% rename from docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile rename to docker/images/parabol-ubi/dockerfiles/basic.dockerfile index 077d95a8fb6..f74101cf9cc 100644 --- a/docker/parabol-ubi/docker-build/dockerfiles/basic.dockerfile +++ b/docker/images/parabol-ubi/dockerfiles/basic.dockerfile @@ -7,8 +7,8 @@ ENV HOME=/home/node \ ENV NPM_CONFIG_PREFIX=/home/node/.npm-global ENV PORT=3000 -COPY --chown=node --chmod=755 docker/parabol-ubi/docker-build/entrypoints/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh -COPY --chown=node docker/parabol-ubi/docker-build/tools/ip-to-server_id ${HOME}/tools/ip-to-server_id +COPY --chown=node --chmod=755 docker/images/parabol-ubi/entrypoints/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +COPY --chown=node docker/images/parabol-ubi/tools/ip-to-server_id ${HOME}/tools/ip-to-server_id # Required for pushToCDN to work with FILE_STORE_PROVIDER set to 'local' RUN mkdir -p ${HOME}/parabol/self-hosted && \ diff --git a/docker/parabol-ubi/docker-build/dockerfiles/parabol.dockerfile b/docker/images/parabol-ubi/dockerfiles/parabol.dockerfile similarity index 100% rename from docker/parabol-ubi/docker-build/dockerfiles/parabol.dockerfile rename to docker/images/parabol-ubi/dockerfiles/parabol.dockerfile diff --git a/docker/parabol-ubi/docker-build/entrypoints/docker-entrypoint.sh b/docker/images/parabol-ubi/entrypoints/docker-entrypoint.sh similarity index 100% rename from docker/parabol-ubi/docker-build/entrypoints/docker-entrypoint.sh rename to docker/images/parabol-ubi/entrypoints/docker-entrypoint.sh diff --git a/docker/parabol-ubi/docker-build/environments/basic-env b/docker/images/parabol-ubi/environments/basic-env similarity index 81% rename from docker/parabol-ubi/docker-build/environments/basic-env rename to docker/images/parabol-ubi/environments/basic-env index 86bbad9252d..7141dd3f371 100644 --- a/docker/parabol-ubi/docker-build/environments/basic-env +++ b/docker/images/parabol-ubi/environments/basic-env @@ -4,7 +4,7 @@ NODE_ENV='production' NODE_EXTRA_CA_CERTS='' PROTO='https' PORT='3000' -# Database configurations must be the same used in the docker-build.yml Github workflow +# Database configurations must be the same used in the build.yml Github workflow POSTGRES_PASSWORD='temppassword' POSTGRES_USER='tempuser' POSTGRES_DB='tempdb' diff --git a/docker/parabol-ubi/docker-build/environments/legacy-build b/docker/images/parabol-ubi/environments/legacy-build similarity index 100% rename from docker/parabol-ubi/docker-build/environments/legacy-build rename to docker/images/parabol-ubi/environments/legacy-build diff --git a/docker/parabol-ubi/docker-build/environments/pipeline b/docker/images/parabol-ubi/environments/pipeline similarity index 93% rename from docker/parabol-ubi/docker-build/environments/pipeline rename to docker/images/parabol-ubi/environments/pipeline index 5641d43725f..cfc707c746b 100644 --- a/docker/parabol-ubi/docker-build/environments/pipeline +++ b/docker/images/parabol-ubi/environments/pipeline @@ -36,7 +36,7 @@ PGADMIN_DEFAULT_EMAIL='' PGADMIN_DEFAULT_PASSWORD='' PGSSLMODE='' PORT='3000' -# Database configurations must be the same used in the docker-build.yml Github workflow +# Database configurations must be the same used in the build.yml Github workflow POSTGRES_PASSWORD='temppassword' POSTGRES_USER='tempuser' POSTGRES_DB='tempdb' diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_account_disable_post_pw_expiration.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_account_disable_post_pw_expiration.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_account_disable_post_pw_expiration.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_account_disable_post_pw_expiration.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_logon_fail_delay.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_logon_fail_delay.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_logon_fail_delay.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_logon_fail_delay.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_max_concurrent_login_sessions.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_max_concurrent_login_sessions.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_max_concurrent_login_sessions.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_max_concurrent_login_sessions.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_maximum_age_login_defs.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_maximum_age_login_defs.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_maximum_age_login_defs.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_maximum_age_login_defs.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_minimum_age_login_defs.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_minimum_age_login_defs.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_minimum_age_login_defs.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_minimum_age_login_defs.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_all_shadowed_sha512.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_all_shadowed_sha512.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_all_shadowed_sha512.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_all_shadowed_sha512.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_minlen_login_defs.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_minlen_login_defs.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_minlen_login_defs.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_minlen_login_defs.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dcredit.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dcredit.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dcredit.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dcredit.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dictcheck.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dictcheck.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dictcheck.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_dictcheck.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_difok.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_difok.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_difok.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_difok.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_lcredit.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_lcredit.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_lcredit.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_lcredit.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxclassrepeat.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxclassrepeat.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxclassrepeat.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxclassrepeat.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxrepeat.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxrepeat.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxrepeat.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_maxrepeat.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minclass.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minclass.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minclass.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minclass.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minlen.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minlen.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minlen.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_minlen.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ocredit.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ocredit.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ocredit.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ocredit.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_password_auth.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_password_auth.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_password_auth.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_password_auth.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_system_auth.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_system_auth.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_system_auth.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_pwhistory_remember_system_auth.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ucredit.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ucredit.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ucredit.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_ucredit.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_unix_remember.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_unix_remember.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_unix_remember.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_password_pam_unix_remember.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny_root.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny_root.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny_root.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny_root.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_interval.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_interval.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_interval.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_interval.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_unlock_time.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_unlock_time.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_unlock_time.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_unlock_time.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_bashrc.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_bashrc.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_bashrc.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_bashrc.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_csh_cshrc.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_csh_cshrc.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_csh_cshrc.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_csh_cshrc.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_login_defs.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_login_defs.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_login_defs.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_login_defs.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_profile.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_profile.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_profile.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_accounts_umask_etc_profile.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_banner_etc_issue.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_banner_etc_issue.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_banner_etc_issue.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_banner_etc_issue.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_configure_crypto_policy.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_configure_crypto_policy.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_configure_crypto_policy.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_configure_crypto_policy.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_configure_kerberos_crypto_policy.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_configure_kerberos_crypto_policy.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_configure_kerberos_crypto_policy.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_configure_kerberos_crypto_policy.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_configure_openssl_crypto_policy.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_configure_openssl_crypto_policy.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_configure_openssl_crypto_policy.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_configure_openssl_crypto_policy.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_configure_usbguard_auditbackend.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_configure_usbguard_auditbackend.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_configure_usbguard_auditbackend.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_configure_usbguard_auditbackend.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_coredump_disable_backtraces.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_coredump_disable_backtraces.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_coredump_disable_backtraces.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_coredump_disable_backtraces.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_coredump_disable_storage.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_coredump_disable_storage.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_coredump_disable_storage.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_coredump_disable_storage.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_burstaction.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_burstaction.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_burstaction.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_burstaction.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_disable_users_coredumps.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_disable_users_coredumps.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_disable_users_coredumps.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_disable_users_coredumps.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_display_login_attempts.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_display_login_attempts.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_display_login_attempts.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_display_login_attempts.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_ensure_gpgcheck_local_packages.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_ensure_gpgcheck_local_packages.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_ensure_gpgcheck_local_packages.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_ensure_gpgcheck_local_packages.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_file_groupowner_var_log_messages.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_file_groupowner_var_log_messages.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_file_groupowner_var_log_messages.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_file_groupowner_var_log_messages.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_file_groupownership_system_commands_dirs.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_file_groupownership_system_commands_dirs.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_file_groupownership_system_commands_dirs.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_file_groupownership_system_commands_dirs.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_file_owner_var_log_messages.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_file_owner_var_log_messages.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_file_owner_var_log_messages.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_file_owner_var_log_messages.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_kernel_module_atm_disabled.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_kernel_module_atm_disabled.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_kernel_module_atm_disabled.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_kernel_module_atm_disabled.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_kernel_module_can_disabled.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_kernel_module_can_disabled.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_kernel_module_can_disabled.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_kernel_module_can_disabled.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_kernel_module_cramfs_disabled.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_kernel_module_cramfs_disabled.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_kernel_module_cramfs_disabled.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_kernel_module_cramfs_disabled.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_kernel_module_firewire-core_disabled.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_kernel_module_firewire-core_disabled.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_kernel_module_firewire-core_disabled.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_kernel_module_firewire-core_disabled.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_kernel_module_sctp_disabled.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_kernel_module_sctp_disabled.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_kernel_module_sctp_disabled.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_kernel_module_sctp_disabled.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_kernel_module_tipc_disabled.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_kernel_module_tipc_disabled.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_kernel_module_tipc_disabled.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_kernel_module_tipc_disabled.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_no_empty_passwords.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_no_empty_passwords.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_no_empty_passwords.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_no_empty_passwords.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_openssl_use_strong_entropy.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_openssl_use_strong_entropy.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_openssl_use_strong_entropy.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_openssl_use_strong_entropy.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_package_crypto-policies_installed.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_package_crypto-policies_installed.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_package_crypto-policies_installed.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_package_crypto-policies_installed.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_package_iptables_installed.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_package_iptables_installed.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_package_iptables_installed.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_package_iptables_installed.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_package_rng-tools_installed.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_package_rng-tools_installed.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_package_rng-tools_installed.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_package_rng-tools_installed.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_package_sudo_installed.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_package_sudo_installed.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_package_sudo_installed.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_package_sudo_installed.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_package_usbguard_installed.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_package_usbguard_installed.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_package_usbguard_installed.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_package_usbguard_installed.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_sudo_require_reauthentication.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_sudo_require_reauthentication.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_sudo_require_reauthentication.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_sudo_require_reauthentication.sh diff --git a/docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_sudoers_validate_passwd.sh b/docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_sudoers_validate_passwd.sh similarity index 100% rename from docker/parabol-ubi/docker-build/security/xccdf_org.ssgproject.content_rule_sudoers_validate_passwd.sh rename to docker/images/parabol-ubi/security/xccdf_org.ssgproject.content_rule_sudoers_validate_passwd.sh diff --git a/docker/parabol-ubi/docker-build/tools/ip-to-server_id/index.js b/docker/images/parabol-ubi/tools/ip-to-server_id/index.js similarity index 100% rename from docker/parabol-ubi/docker-build/tools/ip-to-server_id/index.js rename to docker/images/parabol-ubi/tools/ip-to-server_id/index.js diff --git a/docker/parabol-ubi/docker-build/tools/ip-to-server_id/package.json b/docker/images/parabol-ubi/tools/ip-to-server_id/package.json similarity index 100% rename from docker/parabol-ubi/docker-build/tools/ip-to-server_id/package.json rename to docker/images/parabol-ubi/tools/ip-to-server_id/package.json diff --git a/packages/server/postgres/Dockerfile b/docker/images/postgres/Dockerfile similarity index 72% rename from packages/server/postgres/Dockerfile rename to docker/images/postgres/Dockerfile index e37b75a097e..3ce8d5d5185 100644 --- a/packages/server/postgres/Dockerfile +++ b/docker/images/postgres/Dockerfile @@ -1,15 +1,17 @@ FROM postgres:15.4 +ARG PGVECTOR_VERSION=v0.6.1 +ARG PSQL_MAJOR_VERSION=15 ADD extensions /extensions RUN apt-get update && apt-get install -y \ build-essential \ locales \ - postgresql-server-dev-15 \ + postgresql-server-dev-${PSQL_MAJOR_VERSION} \ git -RUN cd /extensions/postgres-json-schema && make install && make installcheck -RUN git clone --branch v0.5.0 \ +# PGVector +RUN git clone --branch ${PGVECTOR_VERSION} \ https://github.com/pgvector/pgvector.git /extensions/pgvector && \ cd extensions/pgvector && make clean && make && make install diff --git a/docker/images/postgres/extensions/install.sql b/docker/images/postgres/extensions/install.sql new file mode 100644 index 00000000000..5e2d0c13454 --- /dev/null +++ b/docker/images/postgres/extensions/install.sql @@ -0,0 +1 @@ +CREATE EXTENSION IF NOT EXISTS "vector"; diff --git a/docker/stacks/development/README.md b/docker/stacks/development/README.md new file mode 100644 index 00000000000..a14d00d59cb --- /dev/null +++ b/docker/stacks/development/README.md @@ -0,0 +1,43 @@ +# Development stack + +> ⚠️ **Parabol does not provide any support on this stack**. Use it under your own resposibility. + +## General notes + +- **This stack is not meant for production use.** It is our development stack and can change at any moment, have errors and incorporate and remove components we are testing without any notice. +- This stack is designed to be managed using `yarn db:start` and `yarn db:stop` to start the databases. The application can use it, starting with either `yarn dev` or building the application and using `yarn start`. + +## Components + +- **Datadog agent:** additional configuration can be added in the folder `datadog/dd-conf.d`. +- **RethinkDB:** ports 28015 and 8080 (console) exposed to communicate with the cluster. Data mounted in a volume rethinkdb-data. +- **Postgres:** container built from a Dockerfile in [docker/images/postgres](docker/images/postgres), that incorporates some extra extensions used by the application. Exposed through port 5432 and with the data mounted in a volume postgres-data. +- **PGAdmin:** available on 5050 with credentials on the `.env` file. Connect using the values of `PGADMIN_DEFAULT_EMAIL` and `PGADMIN_DEFAULT_PASSWORD` from the `.env`. Data mounted on a volume pgadmin-data. +- **Redis:** available on 6379 with the data mounted on a volume redis-data. +- **Redis Commander:** available on 8081. +- **Text Embedding Inference:** toolkit to deploy and serve open source text embeddings and sequence classification models. Exposed on 3040. More information in [their Github](https://github.com/huggingface/text-embeddings-inference). + +### Configue PGAdmin + +- pgadmin is at [http://localhost:5050](http://localhost:5050) +- Connect using the values of `PGADMIN_DEFAULT_EMAIL` and `PGADMIN_DEFAULT_PASSWORD` from your `.env` +- Click **Add New Server** and fill out the forms with your `.env` values + + - General.name = POSTGRES_DB + - Connection.host = 'postgres' (hardcoded from docker-compose dev.yml, not from .env!) + - Connection.username = POSTGRES_USER + - Connection.password = POSTGRES_PASSWORD + - Connection.maintenanceDatabase = POSTGRES_DB + - Connection.port = POSTGRES_PORT + +### Postgres + +#### Too many connections + +Sometimes pg pool will hit its connection limit. This should never happen in prod, but happens on occassion in dev. +You'll know it's happening because PG will say there are too many connections. +To fix, you can run the following SQL to remove all the connections except the one that is running the script + +```sql +select pg_terminate_backend(pid) from pg_stat_activity where datname='parabol-saas' AND pid <> pg_backend_pid(); +``` diff --git a/docker/dd-conf.d/gqlExecutor.yml b/docker/stacks/development/datadog/dd-conf.d/gqlExecutor.yml similarity index 100% rename from docker/dd-conf.d/gqlExecutor.yml rename to docker/stacks/development/datadog/dd-conf.d/gqlExecutor.yml diff --git a/docker/dd-conf.d/web.yml b/docker/stacks/development/datadog/dd-conf.d/web.yml similarity index 100% rename from docker/dd-conf.d/web.yml rename to docker/stacks/development/datadog/dd-conf.d/web.yml diff --git a/docker/dev.yml b/docker/stacks/development/docker-compose.yml similarity index 76% rename from docker/dev.yml rename to docker/stacks/development/docker-compose.yml index c45ba6b6f6b..91adbbb6578 100644 --- a/docker/dev.yml +++ b/docker/stacks/development/docker-compose.yml @@ -1,10 +1,11 @@ -version: "3.7" +version: "3.9" +name: parabol-dev services: datadog: image: gcr.io/datadoghq/agent:7 restart: unless-stopped - env_file: ../.env + env_file: ../../../.env ports: - "8126:8126" networks: @@ -13,7 +14,9 @@ services: - /var/run/docker.sock:/var/run/docker.sock - /proc/:/host/proc/:ro - /sys/fs/cgroup:/host/sys/fs/cgroup:ro - db: + - ".datadog/dd-conf.d:/etc/datadog-agent/conf.d/local.d/" + - "../../../dev/logs:/var/log/datadog/logs" + rethinkdb: image: rethinkdb:2.4.2 restart: unless-stopped ports: @@ -26,23 +29,20 @@ services: - parabol-network postgres: build: - context: "../packages/server/postgres" + context: "../../images/postgres" restart: unless-stopped - env_file: ../.env + env_file: ../../../.env ports: - "5432:5432" volumes: - - "../packages/server/postgres/postgres.conf:/usr/local/etc/postgres/postgres.conf" - "postgres-data:/var/lib/postgresql/data" - command: "postgres -c config_file=/usr/local/etc/postgres/postgres.conf" networks: - parabol-network pgadmin: - container_name: pgadmin_container - image: dpage/pgadmin4:latest + image: dpage/pgadmin4:8.3 depends_on: - postgres - env_file: ../.env + env_file: ../../../.env volumes: - "pgadmin-data:/var/lib/pgadmin" ports: @@ -60,18 +60,16 @@ services: networks: - parabol-network redis-commander: - container_name: redis_commander - image: ghcr.io/joeferner/redis-commander:latest + image: ghcr.io/joeferner/redis-commander:0.8.1 hostname: redis-commander restart: unless-stopped environment: - REDIS_HOSTS=local:redis:6379 ports: - - "8082:8081" + - "8081:8081" networks: parabol-network: text-embeddings-inference: - container_name: text-embeddings-inference image: ghcr.io/huggingface/text-embeddings-inference:cpu-0.6 command: - "--model-id=llmrails/ember-v1" diff --git a/certs/README.md b/docker/stacks/development/redis/certs/README.md similarity index 74% rename from certs/README.md rename to docker/stacks/development/redis/certs/README.md index 3059e90eead..0a2e2972912 100644 --- a/certs/README.md +++ b/docker/stacks/development/redis/certs/README.md @@ -8,8 +8,10 @@ The certs that are checked into version control are self-signed and safe to shar All env vars should correspond with the vars in the redis instance. In development, that means: -- In the `docker/dev.yml`, add a volume: `bitnami-redis-data: {}` -- In the `docker/dev.yml`, replace the Redis container sections with the following: + +- In the `docker/stacks/development/docker-compose.yml`, add a volume: `bitnami-redis-data: {}` +- In the `docker/stacks/development/docker-compose.yml`, replace the Redis container sections with the following: + ```yaml image: bitnami/redis:7.0-debian-11 environment: @@ -25,9 +27,9 @@ In development, that means: - ../certs:/opt/bitnami/redis/certs ``` -- Vars in .env should match the vars in dev.yml +- Vars in .env should match the vars in `docker/stacks/development/docker-compose.yml` -Any changes to dev.yml require running `yarn db:start` +Any changes to `docker/stacks/development/docker-compose.yml` require running `yarn db:start` REDIS_PASSWORD: Use this if you'd like our app to connect to redis using a password REDIS_TLS_CERT_FILE: The cert file used to authorize clients. Not available in GCP diff --git a/certs/gen-redis-certs.sh b/docker/stacks/development/redis/certs/gen-redis-certs.sh similarity index 100% rename from certs/gen-redis-certs.sh rename to docker/stacks/development/redis/certs/gen-redis-certs.sh diff --git a/certs/redis.crt b/docker/stacks/development/redis/certs/redis.crt similarity index 100% rename from certs/redis.crt rename to docker/stacks/development/redis/certs/redis.crt diff --git a/certs/redis.key b/docker/stacks/development/redis/certs/redis.key similarity index 100% rename from certs/redis.key rename to docker/stacks/development/redis/certs/redis.key diff --git a/certs/redisCA.crt b/docker/stacks/development/redis/certs/redisCA.crt similarity index 100% rename from certs/redisCA.crt rename to docker/stacks/development/redis/certs/redisCA.crt diff --git a/docker/parabol-ubi/docker-host-st/.env.example b/docker/stacks/single-tenant-host/.env.example similarity index 100% rename from docker/parabol-ubi/docker-host-st/.env.example rename to docker/stacks/single-tenant-host/.env.example diff --git a/docker/parabol-ubi/docker-host-st/README.md b/docker/stacks/single-tenant-host/README.md similarity index 88% rename from docker/parabol-ubi/docker-host-st/README.md rename to docker/stacks/single-tenant-host/README.md index af3a0b244ec..59a6a4a8a3e 100644 --- a/docker/parabol-ubi/docker-host-st/README.md +++ b/docker/stacks/single-tenant-host/README.md @@ -1,8 +1,8 @@ # Docker Host Single Tenant (ST) -To run Parabol in single tenant mode (e.g. simple docker-compose on a docker host). +To run Parabol in single tenant mode (e.g. simple docker-compose on a docker host): -1. Build your Parabol UBI using instructions in `docker/ubi/docker-build/README.md` +1. Build your Parabol UBI using instructions in `docker/images/parabol-ubi/README.md` 2. Create a working `.env` from `.env.example` 3. Update docker-compose.yaml `image: #image:tag` with your built image tag from `step (1.)` 4. Run `docker compose --profile databases --profile parabol up -d` to deploy the local stack. You can run `docker compose --profile databases --profile parabol down` to terminate the local stack @@ -31,12 +31,12 @@ This will run `pre-deploy` and thus it will recreate the `web-server` and the `g Some tools are available to debug the databases is needed: -- pgadmin -- redis-commander +- [pgadmin](https://www.pgadmin.org/) +- [redis-commander](https://github.com/joeferner/redis-commander) To operate them use `docker compose up --profile databases --profile database-debug`. ## Running the whole stack - Start the whole stack: `docker compose --profile databases --profile parabol --profile database-debug --profile chronos up -d`. -- Stop the stack: `docker compose --profile databases --profile parabol --profile database-debug --profile chronos down` +- Stop the stack: `docker compose --profile databases --profile parabol --profile database-debug --profile chronos down`. diff --git a/docker/parabol-ubi/docker-host-st/docker-compose.yaml b/docker/stacks/single-tenant-host/docker-compose.yaml similarity index 83% rename from docker/parabol-ubi/docker-host-st/docker-compose.yaml rename to docker/stacks/single-tenant-host/docker-compose.yaml index 52bb8c76a7d..e5f662ed74a 100644 --- a/docker/parabol-ubi/docker-host-st/docker-compose.yaml +++ b/docker/stacks/single-tenant-host/docker-compose.yaml @@ -1,6 +1,19 @@ -version: '3.9' +version: "3.9" services: + rethinkdb: + container_name: rethinkdb + profiles: ["databases"] + image: rethinkdb:2.4.2 + restart: always + ports: + - "8080:8080" + - "29015:29015" + - "28015:28015" + volumes: + - ./data/rethink:/data + networks: + - parabol-network postgres: container_name: postgres profiles: ["databases"] @@ -10,9 +23,9 @@ services: environment: - PGUSER=$POSTGRES_USER ports: - - '5432:5432' + - "5432:5432" volumes: - - './data/postgres/pgdata:/var/lib/postgresql/data' + - "./data/postgres/pgdata:/var/lib/postgresql/data" healthcheck: test: ["CMD-SHELL", "pg_isready", "-d", "$POSTGRES_DB", "-U", "$POSTGRES_USER"] interval: 10s @@ -32,19 +45,6 @@ services: - "5050:80" networks: - parabol-network - rethinkdb: - container_name: rethinkdb - profiles: ["databases"] - image: rethinkdb:2.4.2 - restart: always - ports: - - '8080:8080' - - '29015:29015' - - '28015:28015' - volumes: - - ./data/rethink:/data - networks: - - parabol-network redis: container_name: redis profiles: ["databases"] @@ -56,7 +56,7 @@ services: retries: 5 restart: always ports: - - '6379:6379' + - "6379:6379" volumes: - ./data/redis:/data networks: @@ -78,13 +78,13 @@ services: pre-deploy: container_name: pre-deploy profiles: ["parabol"] - image: us-central1-docker.pkg.dev/prbl-tooling/parabol-production/parabol:v7.15.2 + image: #image:tag command: bash -c "node dist/preDeploy.js" env_file: .env environment: - SERVER_ID=0 volumes: - - './.env:/parabol/.env' + - "./.env:/parabol/.env" depends_on: rethinkdb: condition: service_started @@ -97,14 +97,14 @@ services: chronos: container_name: chronos profiles: ["chronos"] - image: us-central1-docker.pkg.dev/prbl-tooling/parabol-production/parabol:v7.15.2 + image: #image:tag restart: always command: bash -c "node dist/chronos.js" env_file: .env environment: - SERVER_ID=1 volumes: - - './.env:/parabol/.env' + - "./.env:/parabol/.env" depends_on: pre-deploy: condition: service_completed_successfully @@ -119,16 +119,16 @@ services: web-server: container_name: web-server profiles: ["parabol"] - image: us-central1-docker.pkg.dev/prbl-tooling/parabol-production/parabol:v7.15.2 + image: #image:tag restart: always command: bash -c "node dist/web.js" env_file: .env environment: - SERVER_ID=5 ports: - - '3000:3000' + - "3000:3000" volumes: - - './.env:/parabol/.env' + - "./.env:/parabol/.env" depends_on: pre-deploy: condition: service_completed_successfully @@ -143,14 +143,14 @@ services: gql-executor: container_name: gql-executor profiles: ["parabol"] - image: us-central1-docker.pkg.dev/prbl-tooling/parabol-production/parabol:v7.15.2 + image: #image:tag restart: always command: bash -c "node dist/gqlExecutor.js" env_file: .env environment: - SERVER_ID=10 volumes: - - './.env:/parabol/.env' + - "./.env:/parabol/.env" depends_on: pre-deploy: condition: service_completed_successfully diff --git a/docs/alternative-licenses/us-department-of-defense/README.md b/docs/alternative-licenses/us-department-of-defense/README.md index efbdc1d9f1d..330f56931d5 100644 --- a/docs/alternative-licenses/us-department-of-defense/README.md +++ b/docs/alternative-licenses/us-department-of-defense/README.md @@ -82,7 +82,7 @@ $ yarn && yarn build && yarn start - Click "Add New Server" and fill out the forms with your `.env` values - General.name = POSTGRES_DB - - Connection.host = 'postgres' (hardcoded from docker-compose dev.yml, not from .env!) + - Connection.host = 'postgres' (hardcoded from docker-compose `docker/stacks/development/docker-compose.yml`, not from .env!) - Connection.username = POSTGRES_USER - Connection.password = POSTGRES_PASSWORD - Connection.maintenanceDatabase = POSTGRES_DB diff --git a/package.json b/package.json index d228664dd0c..27e1d48c0e2 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.22.2", + "version": "7.22.3", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -30,8 +30,8 @@ "pg:migrate": "node-pg-migrate -f ./packages/server/postgres/pgmConfig.js", "pg:generate": "export $(grep ^POSTGRES_ .env | tr -d \"'\"); yarn kysely-codegen --exclude-pattern \"(PgMigrations|StripeQuantityMismatchLogging)\" --out-file ./packages/server/postgres/pg.d.ts --dialect postgres --url postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB && prettier --write ./packages/server/postgres/pg.d.ts", "pg:restore": "node ./scripts/toolbox/pgRestore.js", - "db:start": "docker compose -f docker/dev.yml up -d", - "db:stop": "docker compose -f docker/dev.yml down", + "db:start": "docker compose -f docker/stacks/development/docker-compose.yml up -d", + "db:stop": "docker compose -f docker/stacks/development/docker-compose.yml down", "db:migrate": "node scripts/migrate.js", "deduplicate": "yarn yarn-deduplicate yarn.lock", "predeploy": "node dist/preDeploy.js", diff --git a/packages/chronos/package.json b/packages/chronos/package.json index cc50069e63f..bd4099d0ee5 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.22.2", + "version": "7.22.3", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -25,6 +25,6 @@ }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.22.2" + "parabol-server": "7.22.3" } } diff --git a/packages/client/components/ActivityLibrary/ActivityCard.tsx b/packages/client/components/ActivityLibrary/ActivityCard.tsx index 5d671b49b3e..59a955d6249 100644 --- a/packages/client/components/ActivityLibrary/ActivityCard.tsx +++ b/packages/client/components/ActivityLibrary/ActivityCard.tsx @@ -35,8 +35,8 @@ export const ActivityCardImage = (props: PropsWithChildren + (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.22.2", + "version": "7.22.3", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/client/utils/makeDefaultTeamName.ts b/packages/client/utils/makeDefaultTeamName.ts deleted file mode 100644 index a9dfbfa857f..00000000000 --- a/packages/client/utils/makeDefaultTeamName.ts +++ /dev/null @@ -1,54 +0,0 @@ -export const DEFAULT_TEAM_NAMES = [ - 'Bug Writers 🪲', - 'Long Meeting Lovers ❣️', - 'Work Procrastinators ⏱️', - 'Eat Lunch at 11AM 🥪', - 'Midday Nap 😴', - 'Show Us Your Cat 🐱', - 'Comb Your Hair for Zoom 💅', - 'Pajama Pants🌛', - 'Highly Caffeinated ☕', - 'Mute Slack & Chill 😎', - 'Friday Afternoon Meetings Should Be Illegal 🙈', - 'MacGuyvers of Fixing Bugs 🛠️', - 'Verified Swifties 🤠', - 'Excel is Hell 🔥', - 'Ice Cold Seltzer ✨', - '5 Minutes Late 😏', - 'Top Chefs 🧑‍🍳', - 'Clean Code or Bust 🧽', - 'Google It 👨‍💻', - 'AI-Generated Image 🦄', - 'Circling Back ⭕', - 'As Per My Last Email 😐', - 'Mute Button 🔕', - 'Comfy Socks 🧦', - 'Show Me the Data ‼️', - "Shakira's Strawberry Jam 🍓", - "5 O'clock Somewhere 🍻", - 'Trending on TikTok 🕺', - 'Sourdough Starter 🍞', - 'Collaboration Station 🤝', - 'Keyboard Warriors 🤺', - 'Sprinting Squirrels 🐿️', - 'Desk Jockeys 🎵', - 'Agenda Avengers 📝', - 'More Cheese 🧀', - 'Make it Work 💁‍♀️', - 'We ❤️ Dogs', - 'Extra Guac 🥑', - 'Copy/Paste 👬', - 'SEO Optimized 🔍', - 'Experience Architects 🏰', - 'User Interfacers 💡', - '404 Error 👾', - 'Commit and Push 🤓', - 'Crocs 4 Life 🐊', - 'Special Coffee Mug ☕' -] - -export const makeDefaultTeamName = (teamId: string) => { - const seed = [...teamId].reduce((prev, cur) => prev + cur.charCodeAt(0), 0) - const idx = seed % DEFAULT_TEAM_NAMES.length - return `Team ${DEFAULT_TEAM_NAMES[idx]!}` -} diff --git a/packages/gql-executor/package.json b/packages/gql-executor/package.json index 9c8df6f3f95..51da1395eb3 100644 --- a/packages/gql-executor/package.json +++ b/packages/gql-executor/package.json @@ -1,6 +1,6 @@ { "name": "gql-executor", - "version": "7.22.2", + "version": "7.22.3", "description": "A Stateless GraphQL Executor", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/gqlExecutor#readme", @@ -27,8 +27,8 @@ }, "dependencies": { "dd-trace": "^4.2.0", - "parabol-client": "7.22.2", - "parabol-server": "7.22.2", + "parabol-client": "7.22.3", + "parabol-server": "7.22.3", "undici": "^5.26.2" } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index fed0f57a2eb..20fcc803893 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -2,7 +2,7 @@ "name": "integration-tests", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.22.2", + "version": "7.22.3", "description": "", "main": "index.js", "scripts": { diff --git a/packages/server/database/README.md b/packages/server/database/README.md index 43981c7b19b..13d527b63d7 100644 --- a/packages/server/database/README.md +++ b/packages/server/database/README.md @@ -10,4 +10,3 @@ Since that time, RethinkDB has occassionally been unstable for us under high loa Since our data is very relational, it made sense to move PostgresQL, which we are actively doing. - Migrations are stored in [`packages/server/database/migrations`](./migrations/) -- RethinkDB Dashboard is at [http://localhost:8080](http://localhost:8080) diff --git a/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts b/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts index a19f962661a..cb4fc7095f6 100644 --- a/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts +++ b/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts @@ -14,7 +14,6 @@ import createNewOrg from './createNewOrg' import createTeamAndLeader from './createTeamAndLeader' import getUsersbyDomain from '../../../postgres/queries/getUsersByDomain' import sendPromptToJoinOrg from '../../../utils/sendPromptToJoinOrg' -import {makeDefaultTeamName} from 'parabol-client/utils/makeDefaultTeamName' import {DataLoaderWorker} from '../../graphql' import acceptTeamInvitation from '../../../safeMutations/acceptTeamInvitation' import isValid from '../../isValid' @@ -137,7 +136,7 @@ const bootstrapNewUser = async ( const validNewTeam = { id: teamId, orgId, - name: makeDefaultTeamName(teamId), + name: `${preferredName}’s Team`, isOnboardTeam: true } const orgName = `${newUser.preferredName}’s Org` diff --git a/packages/server/graphql/mutations/updateTeamName.ts b/packages/server/graphql/mutations/updateTeamName.ts index 0788f2d9895..834c7d59318 100644 --- a/packages/server/graphql/mutations/updateTeamName.ts +++ b/packages/server/graphql/mutations/updateTeamName.ts @@ -10,7 +10,6 @@ import standardError from '../../utils/standardError' import {GQLContext} from '../graphql' import UpdatedTeamInput, {UpdatedTeamInputType} from '../types/UpdatedTeamInput' import UpdateTeamNamePayload from '../types/UpdateTeamNamePayload' -import {makeDefaultTeamName} from 'parabol-client/utils/makeDefaultTeamName' export default { type: UpdateTeamNamePayload, @@ -60,13 +59,7 @@ export default { updatedAt: now } await updateTeamByTeamId(dbUpdate, teamId) - analytics.teamNameChanged( - viewer, - teamId, - oldName, - newName, - makeDefaultTeamName(teamId) === oldName - ) + analytics.teamNameChanged(viewer, teamId, oldName, newName, oldName.endsWith('’s Team')) const data = {teamId} publish(SubscriptionChannel.TEAM, teamId, 'UpdateTeamNamePayload', data, subOptions) diff --git a/packages/server/package.json b/packages/server/package.json index 4fa8018c480..25a503e3254 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.22.2", + "version": "7.22.3", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -124,7 +124,7 @@ "oauth-1.0a": "^2.2.6", "openai": "^4.24.1", "oy-vey": "^0.12.1", - "parabol-client": "7.22.2", + "parabol-client": "7.22.3", "pg": "^8.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/packages/server/postgres/README.md b/packages/server/postgres/README.md index 07047ec37c6..303ec662993 100644 --- a/packages/server/postgres/README.md +++ b/packages/server/postgres/README.md @@ -1,18 +1,5 @@ # PostgreSQL -## Setup - -- pgadmin is at [http://localhost:5050](http://localhost:5050) -- Connect using the values of `PGADMIN_DEFAULT_EMAIL` and `PGADMIN_DEFAULT_PASSWORD` from your `.env` -- Click "Add New Server" and fill out the forms with your `.env` values - - - General.name = POSTGRES_DB - - Connection.host = 'postgres' (hardcoded from docker-compose dev.yml, not from .env!) - - Connection.username = POSTGRES_USER - - Connection.password = POSTGRES_PASSWORD - - Connection.maintenanceDatabase = POSTGRES_DB - - Connection.port = POSTGRES_PORT - ## Migrations This folder contains all the postgres migrations that have been run on the database. @@ -56,13 +43,3 @@ Parameters are capped at 16-bit, so if you're doing a bulk insert, you'll need t In other words, if `# rows * columns per row > 65,535` you need to do it in batches. `pg-protocol` shows this here: Issue here: - -#### Too many connections - -Sometimes pg pool will hit its connection limit. This should never happen in prod, but happens on occassion in dev. -You'll know it's happening because PG will say there are too many connections. -To fix, you can run the following SQL to remove all the connections except the one that is running the script - -```sql -select pg_terminate_backend(pid) from pg_stat_activity where datname='parabol-saas' AND pid <> pg_backend_pid(); -``` diff --git a/packages/server/postgres/extensions/install.sql b/packages/server/postgres/extensions/install.sql deleted file mode 100644 index b2670548069..00000000000 --- a/packages/server/postgres/extensions/install.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE EXTENSION IF NOT EXISTS "postgres-json-schema"; -CREATE EXTENSION IF NOT EXISTS "vector"; diff --git a/packages/server/postgres/extensions/postgres-json-schema/Makefile b/packages/server/postgres/extensions/postgres-json-schema/Makefile deleted file mode 100644 index 52ea8ab56d5..00000000000 --- a/packages/server/postgres/extensions/postgres-json-schema/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -EXTENSION = postgres-json-schema -DATA = postgres-json-schema--0.1.1.sql - -# postgres build stuff -PG_CONFIG = pg_config -PGXS := $(shell $(PG_CONFIG) --pgxs) -include $(PGXS) diff --git a/packages/server/postgres/extensions/postgres-json-schema/postgres-json-schema--0.1.1.sql b/packages/server/postgres/extensions/postgres-json-schema/postgres-json-schema--0.1.1.sql deleted file mode 100644 index 5c2ba846fff..00000000000 --- a/packages/server/postgres/extensions/postgres-json-schema/postgres-json-schema--0.1.1.sql +++ /dev/null @@ -1,259 +0,0 @@ -CREATE OR REPLACE FUNCTION _validate_json_schema_type(type text, data jsonb) RETURNS boolean AS $f$ -BEGIN - IF type = 'integer' THEN - IF jsonb_typeof(data) != 'number' THEN - RETURN false; - END IF; - IF trunc(data::text::numeric) != data::text::numeric THEN - RETURN false; - END IF; - ELSE - IF type != jsonb_typeof(data) THEN - RETURN false; - END IF; - END IF; - RETURN true; -END; -$f$ LANGUAGE 'plpgsql' IMMUTABLE; - - -CREATE OR REPLACE FUNCTION validate_json_schema(schema jsonb, data jsonb, root_schema jsonb DEFAULT NULL) RETURNS boolean AS $f$ -DECLARE - prop text; - item jsonb; - path text[]; - types text[]; - pattern text; - props text[]; -BEGIN - IF root_schema IS NULL THEN - root_schema = schema; - END IF; - - IF schema ? 'type' THEN - IF jsonb_typeof(schema->'type') = 'array' THEN - types = ARRAY(SELECT jsonb_array_elements_text(schema->'type')); - ELSE - types = ARRAY[schema->>'type']; - END IF; - IF (SELECT NOT bool_or(@extschema@._validate_json_schema_type(type, data)) FROM unnest(types) type) THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'properties' THEN - FOR prop IN SELECT jsonb_object_keys(schema->'properties') LOOP - IF data ? prop AND NOT @extschema@.validate_json_schema(schema->'properties'->prop, data->prop, root_schema) THEN - RETURN false; - END IF; - END LOOP; - END IF; - - IF schema ? 'required' AND jsonb_typeof(data) = 'object' THEN - IF NOT ARRAY(SELECT jsonb_object_keys(data)) @> - ARRAY(SELECT jsonb_array_elements_text(schema->'required')) THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'items' AND jsonb_typeof(data) = 'array' THEN - IF jsonb_typeof(schema->'items') = 'object' THEN - FOR item IN SELECT jsonb_array_elements(data) LOOP - IF NOT @extschema@.validate_json_schema(schema->'items', item, root_schema) THEN - RETURN false; - END IF; - END LOOP; - ELSE - IF NOT ( - SELECT bool_and(i > jsonb_array_length(schema->'items') OR @extschema@.validate_json_schema(schema->'items'->(i::int - 1), elem, root_schema)) - FROM jsonb_array_elements(data) WITH ORDINALITY AS t(elem, i) - ) THEN - RETURN false; - END IF; - END IF; - END IF; - - IF jsonb_typeof(schema->'additionalItems') = 'boolean' and NOT (schema->'additionalItems')::text::boolean AND jsonb_typeof(schema->'items') = 'array' THEN - IF jsonb_array_length(data) > jsonb_array_length(schema->'items') THEN - RETURN false; - END IF; - END IF; - - IF jsonb_typeof(schema->'additionalItems') = 'object' THEN - IF NOT ( - SELECT bool_and(@extschema@.validate_json_schema(schema->'additionalItems', elem, root_schema)) - FROM jsonb_array_elements(data) WITH ORDINALITY AS t(elem, i) - WHERE i > jsonb_array_length(schema->'items') - ) THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'minimum' AND jsonb_typeof(data) = 'number' THEN - IF data::text::numeric < (schema->>'minimum')::numeric THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'maximum' AND jsonb_typeof(data) = 'number' THEN - IF data::text::numeric > (schema->>'maximum')::numeric THEN - RETURN false; - END IF; - END IF; - - IF COALESCE((schema->'exclusiveMinimum')::text::bool, FALSE) THEN - IF data::text::numeric = (schema->>'minimum')::numeric THEN - RETURN false; - END IF; - END IF; - - IF COALESCE((schema->'exclusiveMaximum')::text::bool, FALSE) THEN - IF data::text::numeric = (schema->>'maximum')::numeric THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'anyOf' THEN - IF NOT (SELECT bool_or(@extschema@.validate_json_schema(sub_schema, data, root_schema)) FROM jsonb_array_elements(schema->'anyOf') sub_schema) THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'allOf' THEN - IF NOT (SELECT bool_and(@extschema@.validate_json_schema(sub_schema, data, root_schema)) FROM jsonb_array_elements(schema->'allOf') sub_schema) THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'oneOf' THEN - IF 1 != (SELECT COUNT(*) FROM jsonb_array_elements(schema->'oneOf') sub_schema WHERE @extschema@.validate_json_schema(sub_schema, data, root_schema)) THEN - RETURN false; - END IF; - END IF; - - IF COALESCE((schema->'uniqueItems')::text::boolean, false) THEN - IF (SELECT COUNT(*) FROM jsonb_array_elements(data)) != (SELECT count(DISTINCT val) FROM jsonb_array_elements(data) val) THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'additionalProperties' AND jsonb_typeof(data) = 'object' THEN - props := ARRAY( - SELECT key - FROM jsonb_object_keys(data) key - WHERE key NOT IN (SELECT jsonb_object_keys(schema->'properties')) - AND NOT EXISTS (SELECT * FROM jsonb_object_keys(schema->'patternProperties') pat WHERE key ~ pat) - ); - IF jsonb_typeof(schema->'additionalProperties') = 'boolean' THEN - IF NOT (schema->'additionalProperties')::text::boolean AND jsonb_typeof(data) = 'object' AND NOT props <@ ARRAY(SELECT jsonb_object_keys(schema->'properties')) THEN - RETURN false; - END IF; - ELSEIF NOT ( - SELECT bool_and(@extschema@.validate_json_schema(schema->'additionalProperties', data->key, root_schema)) - FROM unnest(props) key - ) THEN - RETURN false; - END IF; - END IF; - - IF schema ? '$ref' THEN - path := ARRAY( - SELECT regexp_replace(regexp_replace(path_part, '~1', '/'), '~0', '~') - FROM UNNEST(regexp_split_to_array(schema->>'$ref', '/')) path_part - ); - -- ASSERT path[1] = '#', 'only refs anchored at the root are supported'; - IF NOT @extschema@.validate_json_schema(root_schema #> path[2:array_length(path, 1)], data, root_schema) THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'enum' THEN - IF NOT EXISTS (SELECT * FROM jsonb_array_elements(schema->'enum') val WHERE val = data) THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'minLength' AND jsonb_typeof(data) = 'string' THEN - IF char_length(data #>> '{}') < (schema->>'minLength')::numeric THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'maxLength' AND jsonb_typeof(data) = 'string' THEN - IF char_length(data #>> '{}') > (schema->>'maxLength')::numeric THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'not' THEN - IF @extschema@.validate_json_schema(schema->'not', data, root_schema) THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'maxProperties' AND jsonb_typeof(data) = 'object' THEN - IF (SELECT count(*) FROM jsonb_object_keys(data)) > (schema->>'maxProperties')::numeric THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'minProperties' AND jsonb_typeof(data) = 'object' THEN - IF (SELECT count(*) FROM jsonb_object_keys(data)) < (schema->>'minProperties')::numeric THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'maxItems' AND jsonb_typeof(data) = 'array' THEN - IF (SELECT count(*) FROM jsonb_array_elements(data)) > (schema->>'maxItems')::numeric THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'minItems' AND jsonb_typeof(data) = 'array' THEN - IF (SELECT count(*) FROM jsonb_array_elements(data)) < (schema->>'minItems')::numeric THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'dependencies' THEN - FOR prop IN SELECT jsonb_object_keys(schema->'dependencies') LOOP - IF data ? prop THEN - IF jsonb_typeof(schema->'dependencies'->prop) = 'array' THEN - IF NOT (SELECT bool_and(data ? dep) FROM jsonb_array_elements_text(schema->'dependencies'->prop) dep) THEN - RETURN false; - END IF; - ELSE - IF NOT @extschema@.validate_json_schema(schema->'dependencies'->prop, data, root_schema) THEN - RETURN false; - END IF; - END IF; - END IF; - END LOOP; - END IF; - - IF schema ? 'pattern' AND jsonb_typeof(data) = 'string' THEN - IF (data #>> '{}') !~ (schema->>'pattern') THEN - RETURN false; - END IF; - END IF; - - IF schema ? 'patternProperties' AND jsonb_typeof(data) = 'object' THEN - FOR prop IN SELECT jsonb_object_keys(data) LOOP - FOR pattern IN SELECT jsonb_object_keys(schema->'patternProperties') LOOP - RAISE NOTICE 'prop %s, pattern %, schema %', prop, pattern, schema->'patternProperties'->pattern; - IF prop ~ pattern AND NOT @extschema@.validate_json_schema(schema->'patternProperties'->pattern, data->prop, root_schema) THEN - RETURN false; - END IF; - END LOOP; - END LOOP; - END IF; - - IF schema ? 'multipleOf' AND jsonb_typeof(data) = 'number' THEN - IF data::text::numeric % (schema->>'multipleOf')::numeric != 0 THEN - RETURN false; - END IF; - END IF; - - RETURN true; -END; -$f$ LANGUAGE 'plpgsql' IMMUTABLE; diff --git a/packages/server/postgres/extensions/postgres-json-schema/postgres-json-schema.control b/packages/server/postgres/extensions/postgres-json-schema/postgres-json-schema.control deleted file mode 100644 index eaaf496b08a..00000000000 --- a/packages/server/postgres/extensions/postgres-json-schema/postgres-json-schema.control +++ /dev/null @@ -1,3 +0,0 @@ -comment = 'Validate JSON schemas' -relocatable = false -default_version = '0.1.1' diff --git a/packages/server/postgres/postgres.conf b/packages/server/postgres/postgres.conf deleted file mode 100644 index 81a24496bf9..00000000000 --- a/packages/server/postgres/postgres.conf +++ /dev/null @@ -1 +0,0 @@ -listen_addresses='*' diff --git a/packages/server/utils/uwsGetIP.ts b/packages/server/utils/uwsGetIP.ts index 034b266a3c9..b41765b100d 100644 --- a/packages/server/utils/uwsGetIP.ts +++ b/packages/server/utils/uwsGetIP.ts @@ -1,7 +1,11 @@ import {HttpRequest, HttpResponse} from 'uWebSockets.js' +const TRUSTED_PROXY_COUNT = Number(process.env.TRUSTED_PROXY_COUNT) +// if TRUSTED_PROXY_COUNT is not configured correctly we fall back to reading the first IP to avoid rate limiting our proxy +const CLIENT_IP_POS = isNaN(TRUSTED_PROXY_COUNT) ? 0 : -1 - TRUSTED_PROXY_COUNT + const uwsGetIP = (res: HttpResponse, req: HttpRequest) => { - const clientIp = req.getHeader('x-forwarded-for')?.split(',')[0] + const clientIp = req.getHeader('x-forwarded-for')?.split(',').at(CLIENT_IP_POS) if (clientIp) return clientIp // returns ipv6 e.g. '0000:0000:0000:0000:0000:ffff:ac11:0001' return Buffer.from(res.getRemoteAddressAsText()).toString() From 76e92a195b35a87a6efe1a6caa19223cf6cb8bad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 14:38:24 -0700 Subject: [PATCH 08/19] chore(release): Test v7.23.1 (#9576) Signed-off-by: dependabot[bot] Signed-off-by: Matt Krick Co-authored-by: Rafa <101704572+rafaelromcar-parabol@users.noreply.github.com> Co-authored-by: Georg Bremer Co-authored-by: Nick O'Ferrall Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> Co-authored-by: Matt Krick Co-authored-by: Jordan Husney Co-authored-by: adaniels-parabol <71724289+adaniels-parabol@users.noreply.github.com> Co-authored-by: snyk-bot Co-authored-by: Dale Bumblis <135627447+dbumblis-parabol@users.noreply.github.com> Co-authored-by: Bartosz Jarocki Co-authored-by: Marcus Wermuth Co-authored-by: Rafael Romero Co-authored-by: Bruce Tian Co-authored-by: github-actions --- .github/reviewers.yml | 4 + .github/workflows/release-to-prod.yml | 4 +- .github/workflows/release-to-staging.yml | 10 +- .github/workflows/snyk-yarn-lock-commit.yml | 28 ---- .github/workflows/synk-yarn-lock-commit.yml | 52 +++++++ .release-please-manifest.json | 2 +- CHANGELOG.md | 45 ++++++ codegen.json | 13 +- package.json | 2 +- packages/chronos/package.json | 4 +- .../ActivityDetailsSidebar.tsx | 56 ++----- .../CreateNewActivity/CreateNewActivity.tsx | 29 ++-- packages/client/components/MeetingOptions.tsx | 62 ++++++++ packages/client/components/MeetingTopBar.tsx | 17 ++- packages/client/components/RetroDrawer.tsx | 118 +++++++++++++++ .../client/components/RetroDrawerRoot.tsx | 28 ++++ .../components/RetroDrawerTemplateCard.tsx | 76 ++++++++++ .../RetroReflectPhase/RetroReflectPhase.tsx | 2 + packages/client/components/ScopePhaseArea.tsx | 9 +- .../TeamPrompt/TeamPromptDrawer.tsx | 2 +- .../TeamPrompt/TeamPromptOptions.tsx | 2 +- packages/client/modules/demo/initDB.ts | 1 - .../MeetingSummaryEmail/RetroTopic.tsx | 12 +- .../components/AddNewPokerTemplate.tsx | 10 +- .../components/AddNewReflectTemplate.tsx | 9 +- .../components/ProviderList/ProviderList.tsx | 14 +- .../components/InvoiceRow/InvoiceRow.tsx | 21 ++- .../userDashboard/helpers/getRallyLink.tsx | 18 +-- .../mutations/AddPokerTemplateMutation.ts | 12 +- .../mutations/AddReflectTemplateMutation.ts | 12 +- .../UpdateMeetingTemplateMutation.ts | 51 +++++++ packages/client/package.json | 10 +- .../subscriptions/MeetingSubscription.ts | 3 + .../client/subscriptions/TeamSubscription.ts | 11 +- packages/client/ui/Menu/Menu.tsx | 32 ++++ packages/client/ui/Menu/MenuItem.tsx | 28 ++++ packages/client/ui/Tooltip/TooltipContent.tsx | 2 +- packages/client/ui/Tooltip/TooltipTrigger.tsx | 2 +- .../{fordwardRadix.tsx => forwardRadix.tsx} | 0 .../client/utils/AzureDevOpsClientManager.ts | 4 +- packages/client/utils/makeMonthDateString.ts | 10 ++ packages/gql-executor/package.json | 6 +- packages/integration-tests/package.json | 2 +- .../__tests__/updateFeatureFlag.test.ts | 10 +- .../graphql/mutations/addPokerTemplate.ts | 142 ------------------ .../graphql/mutations/addReflectTemplate.ts | 142 ------------------ .../helpers/generateGroupSummaries.ts | 8 +- .../typeDefs/updateOrgFeatureFlag.graphql | 1 - .../public/mutations/addPokerTemplate.ts | 135 +++++++++++++++++ .../public/mutations/addReflectTemplate.ts | 128 ++++++++++++++++ .../public/mutations/updateMeetingTemplate.ts | 49 ++++++ .../public/typeDefs/AddPokerTemplate.graphql | 31 ++++ .../typeDefs/AddReflectTemplate.graphql | 31 ++++ .../public/typeDefs/Organization.graphql | 1 - .../public/typeDefs/Subscriptions.graphql | 5 +- .../graphql/public/typeDefs/User.graphql | 12 ++ .../graphql/public/typeDefs/_legacy.graphql | 22 --- .../public/typeDefs/updateFeatureFlag.graphql | 2 - .../typeDefs/updateMeetingTemplate.graphql | 27 ++++ .../public/types/AddPokerTemplateSuccess.ts | 18 +++ .../public/types/AddReflectTemplateSuccess.ts | 18 +++ .../public/types/OrganizationFeatureFlags.ts | 1 - .../types/UpdateMeetingTemplateSuccess.ts | 14 ++ packages/server/graphql/public/types/User.ts | 3 +- .../graphql/public/types/UserFeatureFlags.ts | 1 - packages/server/graphql/queries/team.ts | 8 +- packages/server/graphql/rootMutation.ts | 4 - .../graphql/types/AddPokerTemplatePayload.ts | 22 --- .../types/AddReflectTemplatePayload.ts | 22 --- .../CreateAzureDevOpsAuthorizeUrlPayload.ts | 18 --- packages/server/package.json | 6 +- ...3510512_addFreeCustomTemplatesRemaining.ts | 30 ++++ .../decrementFreeTemplatesRemaining.ts | 18 +++ .../server/utils/AzureDevOpsServerManager.ts | 10 +- yarn.lock | 49 +++--- 75 files changed, 1220 insertions(+), 603 deletions(-) delete mode 100644 .github/workflows/snyk-yarn-lock-commit.yml create mode 100644 .github/workflows/synk-yarn-lock-commit.yml create mode 100644 packages/client/components/MeetingOptions.tsx create mode 100644 packages/client/components/RetroDrawer.tsx create mode 100644 packages/client/components/RetroDrawerRoot.tsx create mode 100644 packages/client/components/RetroDrawerTemplateCard.tsx create mode 100644 packages/client/mutations/UpdateMeetingTemplateMutation.ts create mode 100644 packages/client/ui/Menu/Menu.tsx create mode 100644 packages/client/ui/Menu/MenuItem.tsx rename packages/client/ui/{fordwardRadix.tsx => forwardRadix.tsx} (100%) create mode 100644 packages/client/utils/makeMonthDateString.ts delete mode 100644 packages/server/graphql/mutations/addPokerTemplate.ts delete mode 100644 packages/server/graphql/mutations/addReflectTemplate.ts create mode 100644 packages/server/graphql/public/mutations/addPokerTemplate.ts create mode 100644 packages/server/graphql/public/mutations/addReflectTemplate.ts create mode 100644 packages/server/graphql/public/mutations/updateMeetingTemplate.ts create mode 100644 packages/server/graphql/public/typeDefs/AddPokerTemplate.graphql create mode 100644 packages/server/graphql/public/typeDefs/AddReflectTemplate.graphql create mode 100644 packages/server/graphql/public/typeDefs/updateMeetingTemplate.graphql create mode 100644 packages/server/graphql/public/types/AddPokerTemplateSuccess.ts create mode 100644 packages/server/graphql/public/types/AddReflectTemplateSuccess.ts create mode 100644 packages/server/graphql/public/types/UpdateMeetingTemplateSuccess.ts delete mode 100644 packages/server/graphql/types/AddPokerTemplatePayload.ts delete mode 100644 packages/server/graphql/types/AddReflectTemplatePayload.ts delete mode 100644 packages/server/graphql/types/CreateAzureDevOpsAuthorizeUrlPayload.ts create mode 100644 packages/server/postgres/migrations/1709933510512_addFreeCustomTemplatesRemaining.ts create mode 100644 packages/server/postgres/queries/decrementFreeTemplatesRemaining.ts diff --git a/.github/reviewers.yml b/.github/reviewers.yml index 8d2732ba714..84b51a4d067 100644 --- a/.github/reviewers.yml +++ b/.github/reviewers.yml @@ -34,8 +34,12 @@ files: - data "**/analytics/**": - data + "docker/**": + - devops ".env.example": - devops + "release-please-config.json": + - devops options: ignore_draft: true ignored_keywords: diff --git a/.github/workflows/release-to-prod.yml b/.github/workflows/release-to-prod.yml index 9e964a20718..037676e4f62 100644 --- a/.github/workflows/release-to-prod.yml +++ b/.github/workflows/release-to-prod.yml @@ -31,14 +31,14 @@ jobs: command: | RES=$(curl "https://gitlab.com/api/v4/projects/${{ vars.GITLAB_PROJECT_ID }}/jobs/${{ env.JOB_ID }}/play" \ --request POST \ - --header 'PRIVATE-TOKEN: ${{ secrets.GITLAB_API_TOKEN }}') + --header 'PRIVATE-TOKEN: ${{ secrets.GITLAB_DEPLOYMENT_ACCESS_TOKEN }}') echo $RES JOB_ID_DONE=$(echo $RES | jq '.id // empty') [ -z "$JOB_ID_DONE" ] && exit 1 || exit 0 - name: Poll Production Release uses: artiz/poll-endpoint@1.0.2 with: - url: https://gitlab.com/api/v4/projects/${{ vars.GITLAB_PROJECT_ID }}/jobs/${{ env.JOB_ID }}?access_token=${{ secrets.GITLAB_API_TOKEN }} + url: https://gitlab.com/api/v4/projects/${{ vars.GITLAB_PROJECT_ID }}/jobs/${{ env.JOB_ID }}?access_token=${{ secrets.GITLAB_DEPLOYMENT_ACCESS_TOKEN }} method: GET expect-status: 200 expect-response-regex: '"status":"success"' diff --git a/.github/workflows/release-to-staging.yml b/.github/workflows/release-to-staging.yml index c6e3601f2ff..bc30de202b3 100644 --- a/.github/workflows/release-to-staging.yml +++ b/.github/workflows/release-to-staging.yml @@ -42,7 +42,7 @@ jobs: run: | COMMIT_ID=$(curl "https://gitlab.com/api/v4/projects/${{ vars.GITLAB_PROJECT_ID }}/repository/commits" \ --request POST \ - --header 'PRIVATE-TOKEN: ${{ secrets.GITLAB_API_TOKEN }}' \ + --header 'PRIVATE-TOKEN: ${{ secrets.GITLAB_DEPLOYMENT_ACCESS_TOKEN }}' \ --form "branch=main" \ --form "commit_message=release v${{ env.ACTION_VERSION }}" \ --form "actions[][action]=update" \ @@ -67,11 +67,11 @@ jobs: command: | echo ${{ env.COMMIT_ID }} PIPELINES=$(curl "https://gitlab.com/api/v4/projects/${{ vars.GITLAB_PROJECT_ID }}/pipelines" \ - --header 'PRIVATE-TOKEN: ${{ secrets.GITLAB_API_TOKEN }}') + --header 'PRIVATE-TOKEN: ${{ secrets.GITLAB_DEPLOYMENT_ACCESS_TOKEN }}') PIPELINE_ID=$(echo $PIPELINES | jq ".[] | select(.sha == \"${{ env.COMMIT_ID }}\")" | jq .id) [ -z "$PIPELINE_ID" ] && exit 1 JOBS=$(curl "https://gitlab.com/api/v4/projects/${{ vars.GITLAB_PROJECT_ID }}/pipelines/$PIPELINE_ID/jobs" \ - --header 'PRIVATE-TOKEN: ${{ secrets.GITLAB_API_TOKEN }}') + --header 'PRIVATE-TOKEN: ${{ secrets.GITLAB_DEPLOYMENT_ACCESS_TOKEN }}') JOB_ID=$(echo $JOBS | jq '.[] | select(.name == "${{ env.STAGING_JOB }}")' | jq .id) PROD_JOB_ID=$(echo $JOBS | jq '.[] | select(.name == "${{ env.PRODUCTION_JOB}}")' | jq .id) echo "JOB_ID=${JOB_ID}" >> $GITHUB_ENV @@ -86,7 +86,7 @@ jobs: command: | RES=$(curl "https://gitlab.com/api/v4/projects/${{ vars.GITLAB_PROJECT_ID }}/jobs/${{ env.JOB_ID }}/play" \ --request POST \ - --header 'PRIVATE-TOKEN: ${{ secrets.GITLAB_API_TOKEN }}') + --header 'PRIVATE-TOKEN: ${{ secrets.GITLAB_DEPLOYMENT_ACCESS_TOKEN }}') echo $RES JOB_ID_DONE=$(echo $RES | jq '.id // empty') [ -z "$JOB_ID_DONE" ] && exit 1 || exit 0 @@ -114,7 +114,7 @@ jobs: - name: Poll Staging Release uses: artiz/poll-endpoint@1.0.2 with: - url: https://gitlab.com/api/v4/projects/${{ vars.GITLAB_PROJECT_ID }}/jobs/${{ env.JOB_ID }}?access_token=${{ secrets.GITLAB_API_TOKEN }} + url: https://gitlab.com/api/v4/projects/${{ vars.GITLAB_PROJECT_ID }}/jobs/${{ env.JOB_ID }}?access_token=${{ secrets.GITLAB_DEPLOYMENT_ACCESS_TOKEN }} method: GET expect-status: 200 expect-response-regex: '"status":"success"' diff --git a/.github/workflows/snyk-yarn-lock-commit.yml b/.github/workflows/snyk-yarn-lock-commit.yml deleted file mode 100644 index f4705ec4b79..00000000000 --- a/.github/workflows/snyk-yarn-lock-commit.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Update Snyk PR to add yarn.lock - -on: - pull_request: - types: [opened, synchronize, reopened] - -jobs: - update-snyk-pr: - if: contains(github.event.pull_request.title, '[Snyk]') - runs-on: ubuntu-latest - - steps: - - name: Checkout the repository - uses: actions/checkout@v3 - with: - ref: ${{ github.head_ref }} - token: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, no need to create one - - - name: Install dependencies - run: yarn install - - - name: Commit yarn.lock to the PR branch - run: | - git config --global user.email "action@github.com" - git config --global user.name "GitHub Action" - git add yarn.lock - git commit -m "Update yarn.lock" || echo "No changes to commit" - git push diff --git a/.github/workflows/synk-yarn-lock-commit.yml b/.github/workflows/synk-yarn-lock-commit.yml new file mode 100644 index 00000000000..6ac67f37aa9 --- /dev/null +++ b/.github/workflows/synk-yarn-lock-commit.yml @@ -0,0 +1,52 @@ +name: Update Snyk PR to add yarn.lock + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + update-snyk-pr: + if: contains(github.event.pull_request.title, '[Snyk]') + runs-on: ubuntu-latest + + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, no need to create one + + - name: Setup environment variables + run: | + ACTION_VERSION=$(grep '"version":' package.json | cut -d\" -f4) + echo "ACTION_VERSION=${ACTION_VERSION}" >> $GITHUB_ENV + echo "NODE_VERSION=$(jq -r -j '.engines.node|ltrimstr("^")' package.json)" >> $GITHUB_ENV + + DOCKER_REPOSITORY_FOR_REF=${{ secrets.GCP_AR_PARABOL_DEV }} + echo "DOCKER_REPOSITORY_FOR_REF=${{ secrets.GCP_AR_PARABOL_DEV }}" >> $GITHUB_ENV + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version-file: package.json + # Caching yarn dir & running yarn install is too slow + # Instead, we aggressively cache node_modules below to avoid calling install + + - name: Get cached node modules + id: cache + uses: actions/cache@v3 + with: + path: | + **/node_modules + key: node_modules-${{ runner.arch }}-${{ env.NODE_VERSION }}-${{ hashFiles('yarn.lock') }} + + - name: Install node_modules + run: yarn install + + - name: Commit yarn.lock to the PR branch + run: | + git config --global user.email "action@github.com" + git config --global user.name "GitHub Action" + git add yarn.lock + git commit -m "Update yarn.lock" || echo "No changes to commit" + git push diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f9343c64067..bae38c98366 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.22.3" + ".": "7.23.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index b4b60c13d98..294dbfc8830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,51 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.23.1](https://github.com/ParabolInc/parabol/compare/v7.23.0...v7.23.1) (2024-03-28) + + +### Fixed + +* ensure pool is callable after custom template migration ([#9572](https://github.com/ParabolInc/parabol/issues/9572)) ([0d30206](https://github.com/ParabolInc/parabol/commit/0d30206b154b60f11b23708c9315e9960d4825bb)) +* remove destroyAll from add custom templates migration ([5baf3b7](https://github.com/ParabolInc/parabol/commit/5baf3b7584f8f578284baa7e47eb5e264e99ad53)) + +## [7.23.0](https://github.com/ParabolInc/parabol/compare/v7.22.4...v7.23.0) (2024-03-26) + + +### Added + +* add functionality to change templates during a retro ([#9544](https://github.com/ParabolInc/parabol/issues/9544)) ([e6434e1](https://github.com/ParabolInc/parabol/commit/e6434e181a864b2e61428f55a98994fb1137ac8f)) +* allow 2 custom templates for every user ([#9518](https://github.com/ParabolInc/parabol/issues/9518)) ([2352669](https://github.com/ParabolInc/parabol/commit/2352669ea516a3d764d63af77211fbb4c0a02563)) +* make invoice row title more clear to understand ([#9551](https://github.com/ParabolInc/parabol/issues/9551)) ([9be96eb](https://github.com/ParabolInc/parabol/commit/9be96eb206d367e550b97831621c8b2aee4fc355)) +* release AzureDevOps integration ([#9531](https://github.com/ParabolInc/parabol/issues/9531)) ([87c84a2](https://github.com/ParabolInc/parabol/commit/87c84a2cca1a4d94629291d1325948b3c6cfb790)) +* switch template UI ([#9093](https://github.com/ParabolInc/parabol/issues/9093)) ([2171065](https://github.com/ParabolInc/parabol/commit/21710656b6d689b286759ea495ff334b7ce86adf)) + + +### Fixed + +* **admin:** fix an issue where ORG_ADMIN cannot see members from team they are not in ([#9560](https://github.com/ParabolInc/parabol/issues/9560)) ([ef0fbc2](https://github.com/ParabolInc/parabol/commit/ef0fbc2da853e2248a16ff2a2ce37c1f85f07f1a)) +* Removed broken Rally links and fixed Youtube links ([#9332](https://github.com/ParabolInc/parabol/issues/9332)) ([5e98234](https://github.com/ParabolInc/parabol/commit/5e98234efca84e7ebcb653f3d71d229a88797a8d)) + + +### Changed + +* [Snyk] Upgrade core-js from 3.8.1 to 3.36.0 ([#9519](https://github.com/ParabolInc/parabol/issues/9519)) ([ab47ce4](https://github.com/ParabolInc/parabol/commit/ab47ce46c768f927c9d71d0a52a049df93b51ba4)) +* [Snyk] Upgrade dotenv from 8.0.0 to 8.6.0 ([#9494](https://github.com/ParabolInc/parabol/issues/9494)) ([1e22931](https://github.com/ParabolInc/parabol/commit/1e22931c25c297e4697ea0e585d888c7b2738cfc)) +* [Snyk] Upgrade graphql-typed from 0.6.1 to 0.7.2 ([#9522](https://github.com/ParabolInc/parabol/issues/9522)) ([0ce1384](https://github.com/ParabolInc/parabol/commit/0ce1384b418f2a48971b732b548b9b93f8882e6c)) +* [Snyk] Upgrade react-dom-confetti from 0.0.10 to 0.2.0 ([#9520](https://github.com/ParabolInc/parabol/issues/9520)) ([ef68915](https://github.com/ParabolInc/parabol/commit/ef6891569f468469c64041d0c97555d76c2657d3)) +* [Snyk] Upgrade react-swipeable-views-core from 0.13.1 to 0.14.0 ([#9521](https://github.com/ParabolInc/parabol/issues/9521)) ([3e42d9b](https://github.com/ParabolInc/parabol/commit/3e42d9b155e171ec54f8d4b0cfddc1ace67cb754)) +* fix update snyk pr action ([#9564](https://github.com/ParabolInc/parabol/issues/9564)) ([092e5d9](https://github.com/ParabolInc/parabol/commit/092e5d95bf75ce2db772669971e28f22e6ee8679)) +* **github:** DevOps review if docker folder is modified or release-please-config is changed ([#9562](https://github.com/ParabolInc/parabol/issues/9562)) ([d18d754](https://github.com/ParabolInc/parabol/commit/d18d75485116c883d72456ac51b817a044a38b4d)) +* refactor add template mutation to the new sdl pattern ([#9533](https://github.com/ParabolInc/parabol/issues/9533)) ([fe71841](https://github.com/ParabolInc/parabol/commit/fe718413fa2d19afa660cc944d30a1284d6c2b18)) +* Roll out AIGeneratedDiscussion to all users ([#9554](https://github.com/ParabolInc/parabol/issues/9554)) ([b8fa708](https://github.com/ParabolInc/parabol/commit/b8fa7088e610ab1a920d1b5522cd46ad28e2f715)) + +## [7.22.4](https://github.com/ParabolInc/parabol/compare/v7.22.3...v7.22.4) (2024-03-20) + + +### Changed + +* **ci:** Gitlab deployment access token changed ([4ba2c9e](https://github.com/ParabolInc/parabol/commit/4ba2c9eb059325aedfaf6a4b87fae9054245f83a)) + ## [7.22.3](https://github.com/ParabolInc/parabol/compare/v7.22.2...v7.22.3) (2024-03-19) diff --git a/codegen.json b/codegen.json index f47587c90c1..83f9993f249 100644 --- a/codegen.json +++ b/codegen.json @@ -49,6 +49,9 @@ "ActionMeeting": "../../database/types/MeetingAction#default", "ActionMeetingMember": "../../database/types/ActionMeetingMember#default as ActionMeetingMemberDB", "AddApprovedOrganizationDomainsSuccess": "./types/AddApprovedOrganizationDomainsSuccess#AddApprovedOrganizationDomainsSuccessSource", + "AddReactjiToReactableSuccess": "./types/AddReactjiToReactableSuccess#AddReactjiToReactableSuccessSource", + "AddReflectTemplateSuccess": "./types/AddReflectTemplateSuccess#AddReflectTemplateSuccessSource", + "AddPokerTemplateSuccess": "./types/AddPokerTemplateSuccess#AddPokerTemplateSuccessSource", "AddTranscriptionBotSuccess": "./types/AddTranscriptionBotSuccess#AddTranscriptionBotSuccessSource", "AddedNotification": "./types/AddedNotification#AddedNotificationSource", "AgendaItem": "../../database/types/AgendaItem#default as AgendaItemDB", @@ -72,8 +75,8 @@ "InviteToTeamPayload": "./types/InviteToTeamPayload#InviteToTeamPayloadSource", "JiraIssue": "./types/JiraIssue#JiraIssueSource", "JiraRemoteProject": "../types/JiraRemoteProject#JiraRemoteProjectSource", - "MeetingSeries": "../../postgres/types/MeetingSeries#MeetingSeries", "Kudos": "../../postgres/types/Kudos#Kudos", + "MeetingSeries": "../../postgres/types/MeetingSeries#MeetingSeries", "MeetingTemplate": "../../database/types/MeetingTemplate#default", "NewMeeting": "../../postgres/types/Meeting#AnyMeeting", "NewMeetingPhase": "../../database/types/GenericMeetingPhase #default as GenericMeetingPhaseDB", @@ -81,11 +84,11 @@ "NotificationTeamInvitation": "../../database/types/NotificationTeamInvitation#default as NotificationTeamInvitationDB", "NotifyDiscussionMentioned": "../../database/types/NotificationDiscussionMentioned#default as NotificationDiscussionMentionedDB", "NotifyKickedOut": "../../database/types/NotificationKickedOut#default", + "NotifyMentioned": "../../database/types/NotificationMentioned#default as NotificationMentionedDB", "NotifyPaymentRejected": "../../database/types/NotificationPaymentRejected#default", "NotifyPromoteToOrgLeader": "../../database/types/NotificationPromoteToBillingLeader#default", "NotifyRequestToJoinOrg": "../../database/types/NotificationRequestToJoinOrg#default", "NotifyResponseMentioned": "../../database/types/NotificationResponseMentioned#default as NotificationResponseMentionedDB", - "NotifyMentioned": "../../database/types/NotificationMentioned#default as NotificationMentionedDB", "NotifyResponseReplied": "../../database/types/NotifyResponseReplied#default as NotifyResponseRepliedDB", "NotifyTaskInvolves": "../../database/types/NotificationTaskInvolves#default", "NotifyTeamArchived": "../../database/types/NotificationTeamArchived#default", @@ -94,6 +97,7 @@ "PokerMeeting": "../../database/types/MeetingPoker#default as MeetingPoker", "PokerMeetingMember": "../../database/types/MeetingPokerMeetingMember#default as PokerMeetingMemberDB", "RRule": "rrule#RRule", + "Reactable": "../../database/types/Reactable#Reactable", "ReflectPrompt": "../../database/types/RetrospectivePrompt#default", "ReflectTemplate": "../../database/types/ReflectTemplate#default", "RemoveApprovedOrganizationDomainsSuccess": "./types/RemoveApprovedOrganizationDomainsSuccess#RemoveApprovedOrganizationDomainsSuccessSource", @@ -101,11 +105,10 @@ "RemoveTeamMemberPayload": "./types/RemoveTeamMemberPayload#RemoveTeamMemberPayloadSource", "RequestToJoinDomainSuccess": "./types/RequestToJoinDomainSuccess#RequestToJoinDomainSuccessSource", "ResetReflectionGroupsSuccess": "./types/ResetReflectionGroupsSuccess#ResetReflectionGroupsSuccessSource", - "RetroReflectionGroup": "../../database/types/RetroReflectionGroup#default as RetroReflectionGroupDB", "RetroReflection": "../../database/types/RetroReflection#default as RetroReflectionDB", + "RetroReflectionGroup": "../../database/types/RetroReflectionGroup#default as RetroReflectionGroupDB", "RetrospectiveMeeting": "../../database/types/MeetingRetrospective#default", "RetrospectiveMeetingMember": "../../database/types/RetroMeetingMember#default", - "Reactable": "../../database/types/Reactable#Reactable", "RetrospectiveMeetingSettings": "../../database/types/MeetingSettingsRetrospective#default", "SAML": "./types/SAML#SAMLSource", "SetMeetingSettingsPayload": "../types/SetMeetingSettingsPayload#SetMeetingSettingsPayloadSource", @@ -139,13 +142,13 @@ "UpdateMeetingPromptSuccess": "./types/UpdateMeetingPromptSuccess#UpdateMeetingPromptSuccessSource", "UpdateOrgPayload": "./types/UpdateOrgPayload#UpdateOrgPayloadSource", "UpdateRecurrenceSettingsSuccess": "./types/UpdateRecurrenceSettingsSuccess#UpdateRecurrenceSettingsSuccessSource", + "UpdateMeetingTemplateSuccess": "./types/UpdateMeetingTemplateSuccess#UpdateMeetingTemplateSuccessSource", "UpdateTaskPayload": "./types/UpdateTaskPayload#UpdateTaskPayloadSource", "UpdateTemplateCategorySuccess": "./types/UpdateTemplateCategorySuccess#UpdateTemplateCategorySuccessSource", "UpdateUserProfilePayload": "./types/UpdateUserProfilePayload#UpdateUserProfilePayloadSource", "UpdatedNotification": "./types/AddedNotification#UpdatedNotificationSource", "UpgradeToTeamTierSuccess": "./types/UpgradeToTeamTierSuccess#UpgradeToTeamTierSuccessSource", "UpsertTeamPromptResponseSuccess": "./types/UpsertTeamPromptResponseSuccess#UpsertTeamPromptResponseSuccessSource", - "AddReactjiToReactableSuccess": "./types/AddReactjiToReactableSuccess#AddReactjiToReactableSuccessSource", "User": "../../postgres/types/IUser#default as IUser", "UserLogInPayload": "./types/UserLogInPayload#UserLogInPayloadSource" } diff --git a/package.json b/package.json index 27e1d48c0e2..82179da713f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.22.3", + "version": "7.23.1", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/chronos/package.json b/packages/chronos/package.json index bd4099d0ee5..b2e5061c6bc 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.22.3", + "version": "7.23.1", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -25,6 +25,6 @@ }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.22.3" + "parabol-server": "7.23.1" } } diff --git a/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx b/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx index a686ca89c36..20574555b3b 100644 --- a/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx +++ b/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx @@ -32,7 +32,6 @@ import NewMeetingSettingsToggleCheckIn from '../NewMeetingSettingsToggleCheckIn' import StyledError from '../StyledError' import FlatPrimaryButton from '../FlatPrimaryButton' import NewMeetingActionsCurrentMeetings from '../NewMeetingActionsCurrentMeetings' -import RaisedButton from '../RaisedButton' import NewMeetingTeamPicker from '../NewMeetingTeamPicker' import {AdhocTeamMultiSelect, Option} from '../AdhocTeamMultiSelect/AdhocTeamMultiSelect' import {Select} from '../../ui/Select/Select' @@ -78,7 +77,6 @@ const ActivityDetailsSidebar = (props: Props) => { fragment ActivityDetailsSidebar_viewer on User { featureFlags { adHocTeams - noTemplateLimit } ...AdhocTeamMultiSelect_viewer organizations { @@ -295,14 +293,6 @@ const ActivityDetailsSidebar = (props: Props) => {
) - const handleUpgrade = () => { - SendClientSideEvent(atmosphere, 'Upgrade CTA Clicked', { - upgradeCTALocation: 'publicTemplate', - meetingType: type - }) - history.push(`/me/organizations/${selectedTeam.orgId}/billing`) - } - const meetingNamePlaceholder = type === 'retrospective' ? 'Retro' @@ -396,42 +386,22 @@ const ActivityDetailsSidebar = (props: Props) => { /> )} - {selectedTeam.tier === 'starter' && - !viewer.featureFlags.noTemplateLimit && - !selectedTemplate.isFree ? ( -
-
- Upgrade to the Team Plan to create custom activities unlocking your - team’s ideal workflow. -
- - Upgrade to Team Plan - -
- ) : ( + {type === 'retrospective' && ( <> - {type === 'retrospective' && ( - <> - - - - - )} - {type === 'poker' && ( - - )} - {type === 'action' && ( - - )} + + + )} + {type === 'poker' && ( + + )} + {type === 'action' && ( + + )}
diff --git a/packages/client/components/ActivityLibrary/CreateNewActivity/CreateNewActivity.tsx b/packages/client/components/ActivityLibrary/CreateNewActivity/CreateNewActivity.tsx index 8346b47a252..f105e0e1aab 100644 --- a/packages/client/components/ActivityLibrary/CreateNewActivity/CreateNewActivity.tsx +++ b/packages/client/components/ActivityLibrary/CreateNewActivity/CreateNewActivity.tsx @@ -95,9 +95,8 @@ const SUPPORTED_CUSTOM_ACTIVITIES: SupportedActivity[] = [ const query = graphql` query CreateNewActivityQuery { viewer { - featureFlags { - noTemplateLimit - } + freeCustomRetroTemplatesRemaining + freeCustomPokerTemplatesRemaining preferredTeamId teams { id @@ -135,12 +134,21 @@ export const CreateNewActivity = (props: Props) => { return selectedActivity }) const {viewer} = data - const {teams, preferredTeamId, featureFlags} = viewer + const { + teams, + preferredTeamId, + freeCustomRetroTemplatesRemaining, + freeCustomPokerTemplatesRemaining + } = viewer const [selectedTeam, setSelectedTeam] = useState( teams.find((team) => team.id === preferredTeamId) ?? sortByTier(teams)[0]! ) const {submitting, error, submitMutation, onError, onCompleted} = useMutationProps() const history = useHistory() + const freeCustomTemplatesRemaining = + selectedActivity.type === 'retrospective' + ? freeCustomRetroTemplatesRemaining + : freeCustomPokerTemplatesRemaining const handleCreateRetroTemplate = () => { if (submitting) { @@ -284,16 +292,15 @@ export const CreateNewActivity = (props: Props) => {
{error &&
{error.message}
}
- {selectedTeam.tier === 'starter' && !featureFlags.noTemplateLimit ? ( -
-
- Upgrade to the Team Plan to create custom activities unlocking your team’s - ideal workflow. -
+ {selectedTeam.tier === 'starter' && freeCustomTemplatesRemaining === 0 ? ( +
+ + Upgrade to the Team Plan to create more custom activities + Upgrade to Team Plan diff --git a/packages/client/components/MeetingOptions.tsx b/packages/client/components/MeetingOptions.tsx new file mode 100644 index 00000000000..d0fd3d97a7c --- /dev/null +++ b/packages/client/components/MeetingOptions.tsx @@ -0,0 +1,62 @@ +import React, {useState} from 'react' +import IconLabel from './IconLabel' +import {Menu} from '../ui/Menu/Menu' +import {MenuItem} from '../ui/Menu/MenuItem' +import SwapHorizIcon from '@mui/icons-material/SwapHoriz' +import {OptionsButton} from './TeamPrompt/TeamPromptOptions' +import {Tooltip} from '../ui/Tooltip/Tooltip' +import {TooltipTrigger} from '../ui/Tooltip/TooltipTrigger' +import {TooltipContent} from '../ui/Tooltip/TooltipContent' + +type Props = { + setShowDrawer: (showDrawer: boolean) => void + showDrawer: boolean + hasReflections: boolean +} + +const MeetingOptions = (props: Props) => { + const {setShowDrawer, showDrawer, hasReflections} = props + const [isOpen, setIsOpen] = useState(false) + + const handleClick = () => { + if (hasReflections) return + setShowDrawer(!showDrawer) + } + + const handleMouseEnter = () => { + if (hasReflections) { + setIsOpen(true) + } + } + + const handleMouseLeave = () => { + setIsOpen(false) + } + + return ( + + +
Options
+ + } + > + +
+ + +
{}
+ Change template +
+
+
+ + {'You can only change the template if no reflections have been added.'} + +
+
+ ) +} + +export default MeetingOptions diff --git a/packages/client/components/MeetingTopBar.tsx b/packages/client/components/MeetingTopBar.tsx index a9d37c7e096..287a53012b4 100644 --- a/packages/client/components/MeetingTopBar.tsx +++ b/packages/client/components/MeetingTopBar.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled' import {Comment} from '@mui/icons-material' -import React, {ReactElement, ReactNode} from 'react' +import React, {ReactElement, ReactNode, useState} from 'react' import {PALETTE} from '~/styles/paletteV3' import {meetingAvatarMediaQueries} from '../styles/meeting' import hasToken from '../utils/hasToken' @@ -9,6 +9,7 @@ import makeMinWidthMediaQuery from '../utils/makeMinWidthMediaQuery' import DemoCreateAccountButton from './DemoCreateAccountButton' import PlainButton from './PlainButton/PlainButton' import SidebarToggle from './SidebarToggle' +import RetroDrawerRoot from './RetroDrawerRoot' const localHeaderBreakpoint = makeMinWidthMediaQuery(600) @@ -148,6 +149,7 @@ interface Props { isRightDrawerOpen?: boolean toggleSidebar: () => void toggleDrawer?: () => void + meetingId?: string } const MeetingTopBar = (props: Props) => { @@ -158,10 +160,14 @@ const MeetingTopBar = (props: Props) => { isMeetingSidebarCollapsed, isRightDrawerOpen, toggleDrawer, - toggleSidebar + toggleSidebar, + meetingId } = props const showButton = isDemoRoute() && !hasToken() const showDiscussionButton = toggleDrawer && !isRightDrawerOpen + const [showDrawer, setShowDrawer] = useState(false) + const isOptionsVisible = !!meetingId && !isDemoRoute() + return ( @@ -177,6 +183,13 @@ const MeetingTopBar = (props: Props) => { )} {avatarGroup} + {isOptionsVisible && ( + + )} {showDiscussionButton && toggleDrawer && ( diff --git a/packages/client/components/RetroDrawer.tsx b/packages/client/components/RetroDrawer.tsx new file mode 100644 index 00000000000..6249386cabf --- /dev/null +++ b/packages/client/components/RetroDrawer.tsx @@ -0,0 +1,118 @@ +import {Close} from '@mui/icons-material' +import graphql from 'babel-plugin-relay/macro' +import React, {useEffect} from 'react' +import {PreloadedQuery, usePreloadedQuery} from 'react-relay' +import {Breakpoint, DiscussionThreadEnum} from '../types/constEnums' +import ResponsiveDashSidebar from './ResponsiveDashSidebar' +import MeetingOptions from './MeetingOptions' +import RetroDrawerTemplateCard from './RetroDrawerTemplateCard' +import {Drawer} from './TeamPrompt/TeamPromptDrawer' +import {RetroDrawerQuery} from '../__generated__/RetroDrawerQuery.graphql' +import useBreakpoint from '../hooks/useBreakpoint' + +interface Props { + setShowDrawer: (showDrawer: boolean) => void + showDrawer: boolean + queryRef: PreloadedQuery +} + +const RetroDrawer = (props: Props) => { + const {queryRef, showDrawer, setShowDrawer} = props + const {viewer} = usePreloadedQuery( + graphql` + query RetroDrawerQuery($first: Int!, $type: MeetingTypeEnum!, $meetingId: ID!) { + viewer { + meeting(meetingId: $meetingId) { + ... on RetrospectiveMeeting { + id + reflectionGroups { + id + } + localPhase { + ... on ReflectPhase { + phaseType + } + } + } + } + availableTemplates(first: $first, type: $type) + @connection(key: "RetroDrawer_availableTemplates") { + edges { + node { + ...RetroDrawerTemplateCard_template + id + } + } + } + } + } + `, + queryRef + ) + + const templates = viewer.availableTemplates.edges + const meeting = viewer.meeting + const hasReflections = !!(meeting?.reflectionGroups && meeting.reflectionGroups.length > 0) + const phaseType = meeting?.localPhase?.phaseType + const isMobile = !useBreakpoint(Breakpoint.FUZZY_TABLET) + const isDesktop = useBreakpoint(Breakpoint.SIDEBAR_LEFT) + + const toggleDrawer = () => { + setShowDrawer(!showDrawer) + } + + const handleCloseDrawer = () => { + setShowDrawer(false) + } + + useEffect(() => { + if (hasReflections && showDrawer) { + handleCloseDrawer() + } + }, [hasReflections]) + + if (!phaseType || phaseType !== 'reflect') return null + return ( + <> + + + +
+
+
Templates
+
+ +
+
+ {templates.map((template) => ( + + ))} +
+
+
+ + ) +} +export default RetroDrawer diff --git a/packages/client/components/RetroDrawerRoot.tsx b/packages/client/components/RetroDrawerRoot.tsx new file mode 100644 index 00000000000..58bf63544ff --- /dev/null +++ b/packages/client/components/RetroDrawerRoot.tsx @@ -0,0 +1,28 @@ +import React, {Suspense} from 'react' +import useQueryLoaderNow from '../hooks/useQueryLoaderNow' +import retroDrawerQuery, {RetroDrawerQuery} from '../__generated__/RetroDrawerQuery.graphql' +import RetroDrawer from './RetroDrawer' + +type Props = { + showDrawer: boolean + setShowDrawer: (showDrawer: boolean) => void + meetingId: string +} + +const RetroDrawerRoot = (props: Props) => { + const {showDrawer, setShowDrawer, meetingId} = props + const queryRef = useQueryLoaderNow(retroDrawerQuery, { + first: 200, + type: 'retrospective', + meetingId + }) + return ( + + {queryRef && ( + + )} + + ) +} + +export default RetroDrawerRoot diff --git a/packages/client/components/RetroDrawerTemplateCard.tsx b/packages/client/components/RetroDrawerTemplateCard.tsx new file mode 100644 index 00000000000..73fa7c30e3e --- /dev/null +++ b/packages/client/components/RetroDrawerTemplateCard.tsx @@ -0,0 +1,76 @@ +import {ActivityBadge} from './ActivityLibrary/ActivityBadge' +import {ActivityLibraryCardDescription} from './ActivityLibrary/ActivityLibraryCardDescription' +import graphql from 'babel-plugin-relay/macro' +import React from 'react' +import {useFragment} from 'react-relay' +import {ActivityLibraryCard} from './ActivityLibrary/ActivityLibraryCard' +import {ActivityCardImage} from './ActivityLibrary/ActivityCard' +import {RetroDrawerTemplateCard_template$key} from '~/__generated__/RetroDrawerTemplateCard_template.graphql' +import {CategoryID, CATEGORY_THEMES} from '././ActivityLibrary/Categories' +import UpdateMeetingTemplateMutation from '../mutations/UpdateMeetingTemplateMutation' +import useMutationProps from '../hooks/useMutationProps' +import useAtmosphere from '../hooks/useAtmosphere' + +interface Props { + templateRef: RetroDrawerTemplateCard_template$key + meetingId: string + handleCloseDrawer: () => void +} + +const RetroDrawerTemplateCard = (props: Props) => { + const {templateRef, meetingId, handleCloseDrawer} = props + const {onError, onCompleted} = useMutationProps() + const atmosphere = useAtmosphere() + const template = useFragment( + graphql` + fragment RetroDrawerTemplateCard_template on MeetingTemplate { + ...ActivityLibraryCardDescription_template + id + name + category + illustrationUrl + isFree + } + `, + templateRef + ) + + const handleClick = () => { + UpdateMeetingTemplateMutation( + atmosphere, + { + meetingId: meetingId, + templateId: template.id + }, + {onError, onCompleted} + ) + handleCloseDrawer() + } + + return ( + + Premium + ) : null + } + > + + + + + ) +} +export default RetroDrawerTemplateCard diff --git a/packages/client/components/RetroReflectPhase/RetroReflectPhase.tsx b/packages/client/components/RetroReflectPhase/RetroReflectPhase.tsx index 74a5f422cb3..493ae73247c 100644 --- a/packages/client/components/RetroReflectPhase/RetroReflectPhase.tsx +++ b/packages/client/components/RetroReflectPhase/RetroReflectPhase.tsx @@ -30,6 +30,7 @@ const RetroReflectPhase = (props: Props) => { ...StageTimerDisplay_meeting ...StageTimerControl_meeting ...PhaseItemColumn_meeting + id endedAt localPhase { ...RetroReflectPhase_phase @relay(mask: false) @@ -59,6 +60,7 @@ const RetroReflectPhase = (props: Props) => { { } } } - user { - featureFlags { - azureDevOps - } - } } } `, @@ -142,13 +137,11 @@ const ScopePhaseArea = (props: Props) => { ) const isDesktop = useBreakpoint(Breakpoint.SIDEBAR_LEFT) const {viewerMeetingMember} = meeting - const featureFlags = viewerMeetingMember?.user.featureFlags const gitlabIntegration = viewerMeetingMember?.teamMember.integrations.gitlab const jiraServerIntegration = viewerMeetingMember?.teamMember.integrations.jiraServer const azureDevOpsIntegration = viewerMeetingMember?.teamMember.integrations.azureDevOps const allowAzureDevOps = - (!!azureDevOpsIntegration?.sharedProviders.length || !!azureDevOpsIntegration?.cloudProvider) && - featureFlags?.azureDevOps + !!azureDevOpsIntegration?.sharedProviders.length || !!azureDevOpsIntegration?.cloudProvider const isGitLabProviderAvailable = !!( gitlabIntegration?.cloudProvider?.clientId || gitlabIntegration?.sharedProviders.length ) diff --git a/packages/client/components/TeamPrompt/TeamPromptDrawer.tsx b/packages/client/components/TeamPrompt/TeamPromptDrawer.tsx index e74efce1d6b..df0281c13b3 100644 --- a/packages/client/components/TeamPrompt/TeamPromptDrawer.tsx +++ b/packages/client/components/TeamPrompt/TeamPromptDrawer.tsx @@ -13,7 +13,7 @@ import useBreakpoint from '../../hooks/useBreakpoint' import findStageById from '../../utils/meetings/findStageById' import SendClientSideEvent from '../../utils/SendClientSideEvent' -const Drawer = styled('div')<{isDesktop: boolean; isMobile: boolean; isOpen: boolean}>( +export const Drawer = styled('div')<{isDesktop: boolean; isMobile: boolean; isOpen: boolean}>( ({isDesktop, isMobile, isOpen}) => ({ boxShadow: isDesktop ? desktopSidebarShadow : undefined, backgroundColor: '#FFFFFF', diff --git a/packages/client/components/TeamPrompt/TeamPromptOptions.tsx b/packages/client/components/TeamPrompt/TeamPromptOptions.tsx index f34a7530830..bef32e09be0 100644 --- a/packages/client/components/TeamPrompt/TeamPromptOptions.tsx +++ b/packages/client/components/TeamPrompt/TeamPromptOptions.tsx @@ -14,7 +14,7 @@ import TeamPromptOptionsMenu from './TeamPromptOptionsMenu' const COPIED_TOOLTIP_DURATION_MS = 2000 -const OptionsButton = styled(BaseButton)({ +export const OptionsButton = styled(BaseButton)({ color: PALETTE.SKY_500, display: 'flex', flexDirection: 'column', diff --git a/packages/client/modules/demo/initDB.ts b/packages/client/modules/demo/initDB.ts index 6e7954ef5a4..7d1b673b94c 100644 --- a/packages/client/modules/demo/initDB.ts +++ b/packages/client/modules/demo/initDB.ts @@ -303,7 +303,6 @@ const initDemoOrg = () => { suggestGroups: false, teamsLimit: false, noPromptToJoinOrg: false, - AIGeneratedDiscussionPrompt: false, publicTeams: false }, showConversionModal: false diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/RetroTopic.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/RetroTopic.tsx index f6befe9bb30..abbfc8a2e67 100644 --- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/RetroTopic.tsx +++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/RetroTopic.tsx @@ -90,7 +90,7 @@ const RetroTopic = (props: Props) => { reflections { ...EmailReflectionCard_reflection } - topicSummary: summary + discussionPromptQuestion } discussion { commentCount @@ -121,7 +121,7 @@ const RetroTopic = (props: Props) => { const {reflectionGroup, discussion, id: stageId} = stage const {commentCount, discussionSummary} = discussion - const {reflections, title, voteCount, topicSummary} = reflectionGroup! + const {reflections, title, voteCount, discussionPromptQuestion} = reflectionGroup! const imageSource = isEmail ? 'static' : 'local' const icon = imageSource === 'local' ? 'thumb_up_18.svg' : 'thumb_up_18@3x.png' const src = `${ExternalLinks.EMAIL_CDN}${icon}` @@ -143,16 +143,16 @@ const RetroTopic = (props: Props) => { - {(topicSummary || discussionSummary) && ( + {(discussionPromptQuestion || discussionSummary) && ( - {topicSummary && ( + {discussionPromptQuestion && ( <> - {'🤖 Topic Summary'} + {'🤖 Discussion Question'} - {topicSummary} + {discussionPromptQuestion} )} diff --git a/packages/client/modules/meeting/components/AddNewPokerTemplate.tsx b/packages/client/modules/meeting/components/AddNewPokerTemplate.tsx index 053684315a3..b1e4c9e5206 100644 --- a/packages/client/modules/meeting/components/AddNewPokerTemplate.tsx +++ b/packages/client/modules/meeting/components/AddNewPokerTemplate.tsx @@ -53,9 +53,7 @@ const AddNewPokerTemplate = (props: Props) => { id user { id - featureFlags { - noTemplateLimit - } + freeCustomPokerTemplatesRemaining } } } @@ -63,6 +61,9 @@ const AddNewPokerTemplate = (props: Props) => { teamRef ) const {id: teamId, tier, viewerTeamMember} = team + const {user} = viewerTeamMember || {} + const {freeCustomPokerTemplatesRemaining} = user || {} + const {onError, onCompleted, submitMutation, submitting, error} = useMutationProps() const errorTimerId = useRef() useEffect(() => { @@ -71,7 +72,8 @@ const AddNewPokerTemplate = (props: Props) => { } }, []) const canEditTemplates = - tier !== 'starter' || viewerTeamMember?.user?.featureFlags?.noTemplateLimit + tier !== 'starter' || + (freeCustomPokerTemplatesRemaining && freeCustomPokerTemplatesRemaining > 0) const addNewTemplate = () => { if (submitting) return if (!canEditTemplates) { diff --git a/packages/client/modules/meeting/components/AddNewReflectTemplate.tsx b/packages/client/modules/meeting/components/AddNewReflectTemplate.tsx index 61429332903..4e1bff87056 100644 --- a/packages/client/modules/meeting/components/AddNewReflectTemplate.tsx +++ b/packages/client/modules/meeting/components/AddNewReflectTemplate.tsx @@ -53,9 +53,7 @@ const AddNewReflectTemplate = (props: Props) => { id user { id - featureFlags { - noTemplateLimit - } + freeCustomRetroTemplatesRemaining } } } @@ -63,6 +61,8 @@ const AddNewReflectTemplate = (props: Props) => { teamRef ) const {id: teamId, tier, viewerTeamMember} = team + const {user} = viewerTeamMember || {} + const {freeCustomRetroTemplatesRemaining} = user || {} const {onError, onCompleted, submitMutation, submitting, error} = useMutationProps() const errorTimerId = useRef() useEffect(() => { @@ -71,7 +71,8 @@ const AddNewReflectTemplate = (props: Props) => { } }, []) const canEditTemplates = - tier !== 'starter' || viewerTeamMember?.user?.featureFlags?.noTemplateLimit + tier !== 'starter' || + (freeCustomRetroTemplatesRemaining && freeCustomRetroTemplatesRemaining > 0) const addNewTemplate = () => { if (submitting) return if (!canEditTemplates) { diff --git a/packages/client/modules/teamDashboard/components/ProviderList/ProviderList.tsx b/packages/client/modules/teamDashboard/components/ProviderList/ProviderList.tsx index d8eb7a84485..6f2360fad6b 100644 --- a/packages/client/modules/teamDashboard/components/ProviderList/ProviderList.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderList/ProviderList.tsx @@ -107,9 +107,6 @@ const query = graphql` } } } - featureFlags { - azureDevOps - } } } ` @@ -118,10 +115,6 @@ const ProviderList = (props: Props) => { const {queryRef, retry, teamId} = props const data = usePreloadedQuery(query, queryRef) const {viewer} = data - const { - featureFlags: {azureDevOps: allowAzureDevOps} - } = viewer - const integrations = viewer.teamMember?.integrations const allIntegrations = [ @@ -159,8 +152,7 @@ const ProviderList = (props: Props) => { { name: 'Azure DevOps', connected: !!integrations?.azureDevOps.auth?.accessToken, - component: , - hidden: !allowAzureDevOps + component: }, { name: 'MS Teams', @@ -175,12 +167,12 @@ const ProviderList = (props: Props) => { ] const connectedIntegrations = allIntegrations - .filter((integration) => integration.connected && !integration.hidden) + .filter((integration) => integration.connected) .sort((a, b) => a.name.localeCompare(b.name)) .map((integration) => integration.component) const availableIntegrations = allIntegrations - .filter((integration) => !integration.connected && !integration.hidden) + .filter((integration) => !integration.connected) .sort((a, b) => a.name.localeCompare(b.name)) .map((integration) => integration.component) diff --git a/packages/client/modules/userDashboard/components/InvoiceRow/InvoiceRow.tsx b/packages/client/modules/userDashboard/components/InvoiceRow/InvoiceRow.tsx index 479cca7a33d..6651c90663a 100644 --- a/packages/client/modules/userDashboard/components/InvoiceRow/InvoiceRow.tsx +++ b/packages/client/modules/userDashboard/components/InvoiceRow/InvoiceRow.tsx @@ -9,7 +9,7 @@ import RowInfo from '../../../../components/Row/RowInfo' import RowInfoHeading from '../../../../components/Row/RowInfoHeading' import {PALETTE} from '../../../../styles/paletteV3' import makeDateString from '../../../../utils/makeDateString' -import makeMonthString from '../../../../utils/makeMonthString' +import makeMonthDateString from '../../../../utils/makeMonthDateString' import invoiceLineFormat from '../../../invoice/helpers/invoiceLineFormat' const InvoiceAmount = styled('span')({ @@ -70,6 +70,9 @@ const InvoiceRow = (props: Props) => { brand } endAt + nextPeriodCharges { + nextPeriodEnd + } paidAt payUrl status @@ -77,7 +80,17 @@ const InvoiceRow = (props: Props) => { `, invoiceRef ) - const {id: invoiceId, amountDue, creditCard, endAt, paidAt, payUrl, status} = invoice + const { + id: invoiceId, + amountDue, + creditCard, + endAt, + nextPeriodCharges, + paidAt, + payUrl, + status + } = invoice + const {nextPeriodEnd} = nextPeriodCharges const isEstimate = status === 'UPCOMING' return ( @@ -91,7 +104,9 @@ const InvoiceRow = (props: Props) => { - {makeMonthString(endAt)} + + {makeMonthDateString(endAt)} to {makeMonthDateString(nextPeriodEnd)} + {isEstimate && '*'} diff --git a/packages/client/modules/userDashboard/helpers/getRallyLink.tsx b/packages/client/modules/userDashboard/helpers/getRallyLink.tsx index 961f17965c8..ca7288ca227 100644 --- a/packages/client/modules/userDashboard/helpers/getRallyLink.tsx +++ b/packages/client/modules/userDashboard/helpers/getRallyLink.tsx @@ -40,7 +40,7 @@ const rallyList = [ }, { phrase: 'Don’t Stop Believin’', - link: 'https://youtu.be/Pw3GTTYgEV8' + link: 'https://youtu.be/1k8craCGpgs' }, { phrase: 'Gimme Some New', @@ -72,7 +72,7 @@ const rallyList = [ }, { phrase: 'On With The Show', - link: 'https://youtu.be/4ADh8Fs3YdU' + link: 'https://youtu.be/NijPVAu42aI' }, { phrase: 'Rawr, Tiger', @@ -80,7 +80,7 @@ const rallyList = [ }, { phrase: 'Right Here, Right Now', - link: 'https://youtu.be/F7jSp2xmmEE' + link: 'https://youtu.be/ub747pprmJ8' }, { phrase: 'Ring That Bell', @@ -94,10 +94,6 @@ const rallyList = [ phrase: 'Serve It', link: 'https://youtu.be/0J2QdDbelmY' }, - { - phrase: 'Sharpness Without Effort', - link: 'https://youtu.be/hpeTLTj2tww' - }, { phrase: 'Stronger, Richer, Smarter', link: 'https://youtu.be/Wmc8bQoL-J0' @@ -126,10 +122,6 @@ const rallyList = [ phrase: 'You Bossy', link: 'https://youtu.be/SSgp-IIgr4I' }, - { - phrase: 'You Came To Win', - link: 'https://youtu.be/KZaz7OqyTHQ' - }, { phrase: 'You’re The Smart One', link: 'https://youtu.be/bKQYK7PYQpQ' @@ -138,10 +130,6 @@ const rallyList = [ phrase: 'You’ve Got A Bright Future', link: 'https://youtu.be/kZGvnI37mxk' }, - { - phrase: 'You Must Go Pro', - link: 'https://youtu.be/9mSMTXYj7pQ' - }, { phrase: 'Time To Begin', link: 'https://youtu.be/RYlCVwxoL_g' diff --git a/packages/client/mutations/AddPokerTemplateMutation.ts b/packages/client/mutations/AddPokerTemplateMutation.ts index f8fca48eda5..7de55a43760 100644 --- a/packages/client/mutations/AddPokerTemplateMutation.ts +++ b/packages/client/mutations/AddPokerTemplateMutation.ts @@ -9,7 +9,10 @@ import {AddPokerTemplateMutation_team$data} from '../__generated__/AddPokerTempl import handleAddMeetingTemplate from './handlers/handleAddMeetingTemplate' graphql` - fragment AddPokerTemplateMutation_team on AddPokerTemplatePayload { + fragment AddPokerTemplateMutation_team on AddPokerTemplateSuccess { + user { + freeCustomPokerTemplatesRemaining + } pokerTemplate { ...TemplateSharing_template ...PokerTemplateDetailsTemplate @@ -23,6 +26,11 @@ graphql` const mutation = graphql` mutation AddPokerTemplateMutation($teamId: ID!, $parentTemplateId: ID) { addPokerTemplate(teamId: $teamId, parentTemplateId: $parentTemplateId) { + ... on ErrorPayload { + error { + message + } + } ...AddPokerTemplateMutation_team @relay(mask: false) } } @@ -55,7 +63,7 @@ const AddPokerTemplateMutation: StandardMutation = ( updater: (store) => { const payload = store.getRootField('addPokerTemplate') if (!payload) return - addPokerTemplateTeamUpdater(payload, {atmosphere, store}) + addPokerTemplateTeamUpdater(payload as any, {atmosphere, store}) }, optimisticUpdater: (store) => { const {parentTemplateId, teamId} = variables diff --git a/packages/client/mutations/AddReflectTemplateMutation.ts b/packages/client/mutations/AddReflectTemplateMutation.ts index 4bea393ac0a..0b74c28760a 100644 --- a/packages/client/mutations/AddReflectTemplateMutation.ts +++ b/packages/client/mutations/AddReflectTemplateMutation.ts @@ -8,7 +8,10 @@ import {AddReflectTemplateMutation_team$data} from '../__generated__/AddReflectT import handleAddMeetingTemplate from './handlers/handleAddMeetingTemplate' graphql` - fragment AddReflectTemplateMutation_team on AddReflectTemplatePayload { + fragment AddReflectTemplateMutation_team on AddReflectTemplateSuccess { + user { + freeCustomRetroTemplatesRemaining + } reflectTemplate { ...TemplateSharing_template ...ReflectTemplateDetailsTemplate @@ -22,6 +25,11 @@ graphql` const mutation = graphql` mutation AddReflectTemplateMutation($teamId: ID!, $parentTemplateId: ID) { addReflectTemplate(teamId: $teamId, parentTemplateId: $parentTemplateId) { + ... on ErrorPayload { + error { + message + } + } ...AddReflectTemplateMutation_team @relay(mask: false) } } @@ -54,7 +62,7 @@ const AddReflectTemplateMutation: StandardMutation updater: (store) => { const payload = store.getRootField('addReflectTemplate') if (!payload) return - addReflectTemplateTeamUpdater(payload, {atmosphere, store}) + addReflectTemplateTeamUpdater(payload as any, {atmosphere, store}) }, optimisticUpdater: (store) => { const {parentTemplateId, teamId} = variables diff --git a/packages/client/mutations/UpdateMeetingTemplateMutation.ts b/packages/client/mutations/UpdateMeetingTemplateMutation.ts new file mode 100644 index 00000000000..c4dd1d6951b --- /dev/null +++ b/packages/client/mutations/UpdateMeetingTemplateMutation.ts @@ -0,0 +1,51 @@ +import graphql from 'babel-plugin-relay/macro' +import {commitMutation} from 'react-relay' +import {StandardMutation} from '../types/relayMutations' +import {UpdateMeetingTemplateMutation as TUpdateMeetingTemplateMutation} from '../__generated__/UpdateMeetingTemplateMutation.graphql' + +graphql` + fragment UpdateMeetingTemplateMutation_meeting on UpdateMeetingTemplateSuccess { + meeting { + ... on RetrospectiveMeeting { + id + templateId + phases { + id + ... on ReflectPhase { + reflectPrompts { + id + } + } + } + } + } + } +` + +const mutation = graphql` + mutation UpdateMeetingTemplateMutation($meetingId: ID!, $templateId: ID!) { + updateMeetingTemplate(meetingId: $meetingId, templateId: $templateId) { + ... on ErrorPayload { + error { + message + } + } + ...UpdateMeetingTemplateMutation_meeting @relay(mask: false) + } + } +` + +const UpdateMeetingTemplateMutation: StandardMutation = ( + atmosphere, + variables, + {onError, onCompleted} +) => { + return commitMutation(atmosphere, { + mutation, + variables, + onCompleted, + onError + }) +} + +export default UpdateMeetingTemplateMutation diff --git a/packages/client/package.json b/packages/client/package.json index 2c2882a25a8..3f105e730fb 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.22.3", + "version": "7.23.1", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -101,7 +101,7 @@ "chartjs-adapter-dayjs-3": "^1.2.3", "cleave.js": "^1.6.0", "clsx": "^1.2.1", - "core-js": "3.8.1", + "core-js": "3.36.0", "date-fns": "^2.29.3", "dayjs": "^1.11.3", "dompurify": "^2.4.1", @@ -113,7 +113,7 @@ "fbjs": "^3.0.4", "flatted": "^2.0.1", "graphiql": "^3.0.0", - "graphql-typed": "^0.6.1", + "graphql-typed": "^0.7.2", "hoist-non-react-statics": "^3.3.0", "humanize-duration": "3.29.0", "immutable": "3.8.2", @@ -128,13 +128,13 @@ "react-copy-to-clipboard": "^5.0.0", "react-day-picker": "^8.3.7", "react-dom": "^17.0.2", - "react-dom-confetti": "^0.0.10", + "react-dom-confetti": "^0.2.0", "react-ga4": "^1.4.1", "react-relay": "^14.1.0", "react-router": "^5.0.1", "react-router-dom": "^5.0.1", "react-swipeable-views": "https://github.com/mattkrick/react-swipeable-views/tarball/4f1d3062d6f8939e9889bc6241bb46aa7bc5332d", - "react-swipeable-views-core": "0.13.1", + "react-swipeable-views-core": "0.14.0", "react-swipeable-views-utils": "^0.14.0", "react-textarea-autosize": "^7.1.0", "react-transition-group": "^4.3.0", diff --git a/packages/client/subscriptions/MeetingSubscription.ts b/packages/client/subscriptions/MeetingSubscription.ts index 65e3978b03c..d6f30e0a0f1 100644 --- a/packages/client/subscriptions/MeetingSubscription.ts +++ b/packages/client/subscriptions/MeetingSubscription.ts @@ -150,6 +150,9 @@ const subscription = graphql` UpdateRetroMaxVotesSuccess { ...UpdateRetroMaxVotesMutation_meeting @relay(mask: false) } + UpdateMeetingTemplateSuccess { + ...UpdateMeetingTemplateMutation_meeting @relay(mask: false) + } VoteForReflectionGroupPayload { ...VoteForReflectionGroupMutation_meeting @relay(mask: false) } diff --git a/packages/client/subscriptions/TeamSubscription.ts b/packages/client/subscriptions/TeamSubscription.ts index 6bce58cfa10..10e14740d4c 100644 --- a/packages/client/subscriptions/TeamSubscription.ts +++ b/packages/client/subscriptions/TeamSubscription.ts @@ -17,7 +17,6 @@ import { acceptTeamInvitationTeamUpdater } from '../mutations/AcceptTeamInvitationMutation' import {addAgendaItemUpdater} from '../mutations/AddAgendaItemMutation' -import {addReflectTemplateTeamUpdater} from '../mutations/AddReflectTemplateMutation' import {addReflectTemplatePromptTeamUpdater} from '../mutations/AddReflectTemplatePromptMutation' import {addTeamTeamUpdater} from '../mutations/AddTeamMutation' import {archiveTeamTeamOnNext, archiveTeamTeamUpdater} from '../mutations/ArchiveTeamMutation' @@ -40,6 +39,8 @@ import {updateAgendaItemUpdater} from '../mutations/UpdateAgendaItemMutation' import subscriptionOnNext from './subscriptionOnNext' import subscriptionUpdater from './subscriptionUpdater' import {batchArchiveTasksTaskUpdater} from '../mutations/BatchArchiveTasksMutation' +import {addReflectTemplateTeamUpdater} from '../mutations/AddReflectTemplateMutation' +import {addPokerTemplateTeamUpdater} from '../mutations/AddPokerTemplateMutation' const subscription = graphql` subscription TeamSubscription { @@ -66,9 +67,12 @@ const subscription = graphql` AddAtlassianAuthPayload { ...AddAtlassianAuthMutation_team @relay(mask: false) } - AddReflectTemplatePayload { + AddReflectTemplateSuccess { ...AddReflectTemplateMutation_team @relay(mask: false) } + AddPokerTemplateSuccess { + ...AddPokerTemplateMutation_team @relay(mask: false) + } AddReflectTemplatePromptPayload { ...AddReflectTemplatePromptMutation_team @relay(mask: false) } @@ -207,7 +211,8 @@ const updateHandlers = { RemoveAgendaItemPayload: removeAgendaItemUpdater, UpdateAgendaItemPayload: updateAgendaItemUpdater, AcceptTeamInvitationPayload: acceptTeamInvitationTeamUpdater, - AddReflectTemplatePayload: addReflectTemplateTeamUpdater, + AddReflectTemplateSuccess: addReflectTemplateTeamUpdater, + AddPokerTemplateSuccess: addPokerTemplateTeamUpdater, AddReflectTemplatePromptPayload: addReflectTemplatePromptTeamUpdater, AddTeamMutationPayload: addTeamTeamUpdater, ArchiveTeamPayload: archiveTeamTeamUpdater, diff --git a/packages/client/ui/Menu/Menu.tsx b/packages/client/ui/Menu/Menu.tsx new file mode 100644 index 00000000000..47afbbbf35b --- /dev/null +++ b/packages/client/ui/Menu/Menu.tsx @@ -0,0 +1,32 @@ +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import React from 'react' +import {twMerge} from 'tailwind-merge' + +interface MenuProps { + trigger: React.ReactNode + className?: string + children: React.ReactNode +} + +export const Menu = React.forwardRef( + ({trigger, className, children}, ref) => { + return ( + + {trigger} + + + {children} + + + + ) + } +) diff --git a/packages/client/ui/Menu/MenuItem.tsx b/packages/client/ui/Menu/MenuItem.tsx new file mode 100644 index 00000000000..fe117676eb0 --- /dev/null +++ b/packages/client/ui/Menu/MenuItem.tsx @@ -0,0 +1,28 @@ +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import React from 'react' +import {twMerge} from 'tailwind-merge' + +interface MenuItemProps { + onClick: (event: Event) => void + isDisabled?: boolean + className?: string + children: React.ReactNode +} + +export const MenuItem = React.forwardRef( + ({onClick, isDisabled, className, children}, ref) => { + return ( + + {children} + + ) + } +) diff --git a/packages/client/ui/Tooltip/TooltipContent.tsx b/packages/client/ui/Tooltip/TooltipContent.tsx index 03f77bea4ef..c119f4a2b70 100644 --- a/packages/client/ui/Tooltip/TooltipContent.tsx +++ b/packages/client/ui/Tooltip/TooltipContent.tsx @@ -1,7 +1,7 @@ import {Content, Portal} from '@radix-ui/react-tooltip' import * as React from 'react' import {twMerge} from 'tailwind-merge' -import {forwardRadix} from '../fordwardRadix' +import {forwardRadix} from '../forwardRadix' export const TooltipContent = forwardRadix( ({className, children, ...props}, ref) => ( diff --git a/packages/client/ui/Tooltip/TooltipTrigger.tsx b/packages/client/ui/Tooltip/TooltipTrigger.tsx index b364812034c..2a0b7b0ca36 100644 --- a/packages/client/ui/Tooltip/TooltipTrigger.tsx +++ b/packages/client/ui/Tooltip/TooltipTrigger.tsx @@ -1,6 +1,6 @@ import {Trigger} from '@radix-ui/react-tooltip' import * as React from 'react' -import {forwardRadix} from '../fordwardRadix' +import {forwardRadix} from '../forwardRadix' export const TooltipTrigger = forwardRadix( ({className, children, ...props}, ref) => ( diff --git a/packages/client/ui/fordwardRadix.tsx b/packages/client/ui/forwardRadix.tsx similarity index 100% rename from packages/client/ui/fordwardRadix.tsx rename to packages/client/ui/forwardRadix.tsx diff --git a/packages/client/utils/AzureDevOpsClientManager.ts b/packages/client/utils/AzureDevOpsClientManager.ts index 1ad3db655fb..09dc2e510bf 100644 --- a/packages/client/utils/AzureDevOpsClientManager.ts +++ b/packages/client/utils/AzureDevOpsClientManager.ts @@ -40,8 +40,8 @@ class AzureDevOpsClientManager { const providerState = Math.random().toString(36).substring(5) const verifier = AzureDevOpsClientManager.generateVerifier() const code = await AzureDevOpsClientManager.generateCodeChallenge(verifier) - const redirect = makeHref('/auth/ado') - const scope = '499b84ac-1321-427f-aa17-267ca6975798/.default' + const redirect = makeHref('/auth/ado2') + const scope = '499b84ac-1321-427f-aa17-267ca6975798/.default offline_access' const url = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirect}&response_mode=query&scope=${scope}&state=${providerState}&code_challenge=${code}&code_challenge_method=S256` // Open synchronously because of Safari diff --git a/packages/client/utils/makeMonthDateString.ts b/packages/client/utils/makeMonthDateString.ts new file mode 100644 index 00000000000..fd242ebffcc --- /dev/null +++ b/packages/client/utils/makeMonthDateString.ts @@ -0,0 +1,10 @@ +import ensureDate from './ensureDate' + +export default function makeMonthDateString(datetime: Date | string | null) { + const timestamp = ensureDate(datetime) + return timestamp.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) +} diff --git a/packages/gql-executor/package.json b/packages/gql-executor/package.json index 51da1395eb3..bed8c3473eb 100644 --- a/packages/gql-executor/package.json +++ b/packages/gql-executor/package.json @@ -1,6 +1,6 @@ { "name": "gql-executor", - "version": "7.22.3", + "version": "7.23.1", "description": "A Stateless GraphQL Executor", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/gqlExecutor#readme", @@ -27,8 +27,8 @@ }, "dependencies": { "dd-trace": "^4.2.0", - "parabol-client": "7.22.3", - "parabol-server": "7.22.3", + "parabol-client": "7.23.1", + "parabol-server": "7.23.1", "undici": "^5.26.2" } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 20fcc803893..2cbc84391c1 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -2,7 +2,7 @@ "name": "integration-tests", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.22.3", + "version": "7.23.1", "description": "", "main": "index.js", "scripts": { diff --git a/packages/server/__tests__/updateFeatureFlag.test.ts b/packages/server/__tests__/updateFeatureFlag.test.ts index 5bafe858aa4..f252e553b55 100644 --- a/packages/server/__tests__/updateFeatureFlag.test.ts +++ b/packages/server/__tests__/updateFeatureFlag.test.ts @@ -12,7 +12,7 @@ const UPDATE_FEATURE_FLAG = ` users { id featureFlags { - azureDevOps + noAISummary } } } @@ -27,7 +27,7 @@ test('Add feature flag by email', async () => { query: UPDATE_FEATURE_FLAG, variables: { emails: [email], - flag: 'azureDevOps', + flag: 'noAISummary', addFlag: true }, authToken @@ -41,7 +41,7 @@ test('Add feature flag by email', async () => { { id: userId, featureFlags: { - azureDevOps: true + noAISummary: true } } ] @@ -58,7 +58,7 @@ test('Remove feature flag by email', async () => { query: UPDATE_FEATURE_FLAG, variables: { emails: [email], - flag: 'azureDevOps', + flag: 'noAISummary', addFlag: false }, authToken @@ -72,7 +72,7 @@ test('Remove feature flag by email', async () => { { id: userId, featureFlags: { - azureDevOps: false + noAISummary: false } } ] diff --git a/packages/server/graphql/mutations/addPokerTemplate.ts b/packages/server/graphql/mutations/addPokerTemplate.ts deleted file mode 100644 index 3b4e6dbccc0..00000000000 --- a/packages/server/graphql/mutations/addPokerTemplate.ts +++ /dev/null @@ -1,142 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SprintPokerDefaults, SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import PokerTemplate from '../../database/types/PokerTemplate' -import TemplateDimension from '../../database/types/TemplateDimension' -import insertMeetingTemplate from '../../postgres/queries/insertMeetingTemplate' -import {getUserId, isTeamMember, isUserInOrg} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import AddPokerTemplatePayload from '../types/AddPokerTemplatePayload' -import getTemplateIllustrationUrl from './helpers/getTemplateIllustrationUrl' -import {analytics} from '../../utils/analytics/analytics' -import {getFeatureTier} from '../types/helpers/getFeatureTier' - -const addPokerTemplate = { - description: 'Add a new poker template with a default dimension created', - type: new GraphQLNonNull(AddPokerTemplatePayload), - args: { - parentTemplateId: { - type: GraphQLID - }, - teamId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - async resolve( - _source: unknown, - {parentTemplateId, teamId}: {parentTemplateId?: string | null; teamId: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const viewerId = getUserId(authToken) - - // AUTH - if (!isTeamMember(authToken, teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // VALIDATION - const [allTemplates, viewerTeam, viewer] = await Promise.all([ - dataLoader.get('meetingTemplatesByType').load({meetingType: 'poker', teamId}), - dataLoader.get('teams').load(teamId), - dataLoader.get('users').loadNonNull(viewerId) - ]) - - if (!viewerTeam) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - if ( - getFeatureTier(viewerTeam) === 'starter' && - !viewer.featureFlags.includes('noTemplateLimit') - ) { - return standardError(new Error('Creating templates is a premium feature'), {userId: viewerId}) - } - let data - if (parentTemplateId) { - const parentTemplate = await dataLoader.get('meetingTemplates').load(parentTemplateId) - if (!parentTemplate) { - return standardError(new Error('Parent template not found'), {userId: viewerId}) - } - const {name, scope} = parentTemplate - if (scope === 'TEAM') { - if (!isTeamMember(authToken, parentTemplate.teamId)) - return standardError(new Error('Template is scoped to team'), {userId: viewerId}) - } else if (scope === 'ORGANIZATION') { - const parentTemplateTeam = await dataLoader.get('teams').load(parentTemplate.teamId) - const isInOrg = - parentTemplateTeam && - (await isUserInOrg(getUserId(authToken), parentTemplateTeam?.orgId, dataLoader)) - if (!isInOrg) { - return standardError(new Error('Template is scoped to organization'), {userId: viewerId}) - } - } - const copyName = `${name} Copy` - const existingCopyCount = allTemplates.filter((template) => - template.name.startsWith(copyName) - ).length - const newName = existingCopyCount === 0 ? copyName : `${copyName} #${existingCopyCount + 1}` - const newTemplate = new PokerTemplate({ - name: newName, - teamId, - orgId: viewerTeam.orgId, - parentTemplateId, - mainCategory: parentTemplate.mainCategory, - illustrationUrl: parentTemplate.illustrationUrl - }) - - const dimensions = await dataLoader - .get('templateDimensionsByTemplateId') - .load(parentTemplate.id) - const activeDimensions = dimensions.filter(({removedAt}: TemplateDimension) => !removedAt) - const newTemplateDimensions = activeDimensions.map((dimension: TemplateDimension) => { - return new TemplateDimension({ - ...dimension, - teamId, - templateId: newTemplate.id - }) - }) - - await Promise.all([ - r.table('TemplateDimension').insert(newTemplateDimensions).run(), - insertMeetingTemplate(newTemplate) - ]) - analytics.templateMetrics(viewer, newTemplate, 'Template Cloned') - data = {templateId: newTemplate.id} - } else { - const {orgId} = viewerTeam - - const templateCount = allTemplates.length - const newTemplate = new PokerTemplate({ - name: `*New Template #${templateCount + 1}`, - teamId, - orgId, - mainCategory: 'estimation', - illustrationUrl: getTemplateIllustrationUrl('estimatedEffortTemplate.png') - }) - const templateId = newTemplate.id - const newDimension = new TemplateDimension({ - scaleId: SprintPokerDefaults.DEFAULT_SCALE_ID, - description: '', - sortOrder: 0, - name: '*New Dimension', - teamId, - templateId - }) - - await Promise.all([ - r.table('TemplateDimension').insert(newDimension).run(), - insertMeetingTemplate(newTemplate) - ]) - analytics.templateMetrics(viewer, newTemplate, 'Template Created') - data = {templateId} - } - publish(SubscriptionChannel.TEAM, teamId, 'AddPokerTemplatePayload', data, subOptions) - return data - } -} - -export default addPokerTemplate diff --git a/packages/server/graphql/mutations/addReflectTemplate.ts b/packages/server/graphql/mutations/addReflectTemplate.ts deleted file mode 100644 index d4c3825338a..00000000000 --- a/packages/server/graphql/mutations/addReflectTemplate.ts +++ /dev/null @@ -1,142 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import {PALETTE} from '../../../client/styles/paletteV3' -import getRethink from '../../database/rethinkDriver' -import ReflectTemplate from '../../database/types/ReflectTemplate' -import RetrospectivePrompt from '../../database/types/RetrospectivePrompt' -import insertMeetingTemplate from '../../postgres/queries/insertMeetingTemplate' -import {getUserId, isTeamMember, isUserInOrg} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import AddReflectTemplatePayload from '../types/AddReflectTemplatePayload' -import makeRetroTemplates from './helpers/makeRetroTemplates' -import {analytics} from '../../utils/analytics/analytics' -import {getFeatureTier} from '../types/helpers/getFeatureTier' - -const addReflectTemplate = { - description: 'Add a new template full of prompts', - type: AddReflectTemplatePayload, - args: { - parentTemplateId: { - type: GraphQLID - }, - teamId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - async resolve( - _source: unknown, - {parentTemplateId, teamId}: {parentTemplateId?: string | null; teamId: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const viewerId = getUserId(authToken) - - // AUTH - if (!isTeamMember(authToken, teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // VALIDATION - const [allTemplates, viewerTeam, viewer] = await Promise.all([ - dataLoader.get('meetingTemplatesByType').load({meetingType: 'retrospective', teamId}), - dataLoader.get('teams').load(teamId), - dataLoader.get('users').loadNonNull(viewerId) - ]) - - if (!viewerTeam) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - if ( - getFeatureTier(viewerTeam) === 'starter' && - !viewer.featureFlags.includes('noTemplateLimit') - ) { - return standardError(new Error('Creating templates is a premium feature'), {userId: viewerId}) - } - let data - if (parentTemplateId) { - const parentTemplate = await dataLoader.get('meetingTemplates').load(parentTemplateId) - if (!parentTemplate) { - return standardError(new Error('Parent template not found'), {userId: viewerId}) - } - const {name, scope} = parentTemplate - if (scope === 'TEAM') { - if (!isTeamMember(authToken, parentTemplate.teamId)) - return standardError(new Error('Template is scoped to team'), {userId: viewerId}) - } else if (scope === 'ORGANIZATION') { - const parentTemplateTeam = await dataLoader.get('teams').load(parentTemplate.teamId) - const isInOrg = - parentTemplateTeam && - (await isUserInOrg(getUserId(authToken), parentTemplateTeam?.orgId, dataLoader)) - if (!isInOrg) { - return standardError(new Error('Template is scoped to organization'), {userId: viewerId}) - } - } - const copyName = `${name} Copy` - const existingCopyCount = allTemplates.filter((template) => - template.name.startsWith(copyName) - ).length - const newName = existingCopyCount === 0 ? copyName : `${copyName} #${existingCopyCount + 1}` - const newTemplate = new ReflectTemplate({ - name: newName, - teamId, - orgId: viewerTeam.orgId, - parentTemplateId, - illustrationUrl: parentTemplate.illustrationUrl, - mainCategory: parentTemplate.mainCategory - }) - const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(parentTemplate.id) - const activePrompts = prompts.filter(({removedAt}: RetrospectivePrompt) => !removedAt) - const newTemplatePrompts = activePrompts.map((prompt: RetrospectivePrompt) => { - return new RetrospectivePrompt({ - ...prompt, - teamId, - templateId: newTemplate.id, - parentPromptId: prompt.id, - removedAt: null - }) - }) - - await Promise.all([ - r.table('ReflectPrompt').insert(newTemplatePrompts).run(), - insertMeetingTemplate(newTemplate) - ]) - analytics.templateMetrics(viewer, newTemplate, 'Template Cloned') - data = {templateId: newTemplate.id} - } else { - const {orgId} = viewerTeam - // RESOLUTION - const templateCount = allTemplates.length - const base = { - [`*New Template #${templateCount + 1}`]: [ - { - question: 'New prompt', - description: '', - groupColor: PALETTE.JADE_400 - } - ] - } as const - const {reflectPrompts: newTemplatePrompts, templates} = makeRetroTemplates( - teamId, - orgId, - base - ) - // guaranteed since base has 1 key - const newTemplate = templates[0]! - const {id: templateId} = newTemplate - await Promise.all([ - r.table('ReflectPrompt').insert(newTemplatePrompts).run(), - insertMeetingTemplate(newTemplate) - ]) - analytics.templateMetrics(viewer, newTemplate, 'Template Created') - data = {templateId} - } - publish(SubscriptionChannel.TEAM, teamId, 'AddReflectTemplatePayload', data, subOptions) - return data - } -} - -export default addReflectTemplate diff --git a/packages/server/graphql/mutations/helpers/generateGroupSummaries.ts b/packages/server/graphql/mutations/helpers/generateGroupSummaries.ts index 86cd732709d..3a827ca20d1 100644 --- a/packages/server/graphql/mutations/helpers/generateGroupSummaries.ts +++ b/packages/server/graphql/mutations/helpers/generateGroupSummaries.ts @@ -15,7 +15,6 @@ const generateGroupSummaries = async ( dataLoader.get('users').loadNonNull(facilitatorUserId), dataLoader.get('teams').loadNonNull(teamId) ]) - const organization = await dataLoader.get('organizations').load(team.orgId) const isAISummaryAccessible = await canAccessAISummary( team, facilitator.featureFlags, @@ -35,9 +34,6 @@ const generateGroupSummaries = async ( sendToSentry(error, {userId: facilitator.id, tags: {meetingId}}) return } - const aiGeneratedDiscussionPromptEnabled = organization.featureFlags?.includes( - 'AIGeneratedDiscussionPrompt' - ) await Promise.all( reflectionGroups.map(async (group) => { const reflectionsByGroupId = reflections.filter( @@ -49,9 +45,7 @@ const generateGroupSummaries = async ( ) const [fullSummary, fullQuestion] = await Promise.all([ manager.getSummary(reflectionTextByGroupId), - aiGeneratedDiscussionPromptEnabled - ? manager.getDiscussionPromptQuestion(group.title ?? 'Unknown', reflectionsByGroupId) - : undefined + manager.getDiscussionPromptQuestion(group.title ?? 'Unknown', reflectionsByGroupId) ]) if (!fullSummary && !fullQuestion) return const summary = fullSummary?.slice(0, 2000) diff --git a/packages/server/graphql/private/typeDefs/updateOrgFeatureFlag.graphql b/packages/server/graphql/private/typeDefs/updateOrgFeatureFlag.graphql index 211113f9903..925a360df83 100644 --- a/packages/server/graphql/private/typeDefs/updateOrgFeatureFlag.graphql +++ b/packages/server/graphql/private/typeDefs/updateOrgFeatureFlag.graphql @@ -3,7 +3,6 @@ A flag to give an individual organization super powers """ enum OrganizationFeatureFlagsEnum { noAISummary - AIGeneratedDiscussionPrompt standupAISummary noPromptToJoinOrg suggestGroups diff --git a/packages/server/graphql/public/mutations/addPokerTemplate.ts b/packages/server/graphql/public/mutations/addPokerTemplate.ts new file mode 100644 index 00000000000..2141a7e5833 --- /dev/null +++ b/packages/server/graphql/public/mutations/addPokerTemplate.ts @@ -0,0 +1,135 @@ +import {SprintPokerDefaults, SubscriptionChannel} from 'parabol-client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import PokerTemplate from '../../../database/types/PokerTemplate' +import TemplateDimension from '../../../database/types/TemplateDimension' +import insertMeetingTemplate from '../../../postgres/queries/insertMeetingTemplate' +import {getUserId, isTeamMember, isUserInOrg} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import getTemplateIllustrationUrl from '../../mutations/helpers/getTemplateIllustrationUrl' +import {analytics} from '../../../utils/analytics/analytics' +import {getFeatureTier} from '../../types/helpers/getFeatureTier' +import decrementFreeTemplatesRemaining from '../../../postgres/queries/decrementFreeTemplatesRemaining' +import {MutationResolvers} from '../resolverTypes' + +const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( + _source, + {teamId, parentTemplateId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const viewerId = getUserId(authToken) + + // AUTH + if (!isTeamMember(authToken, teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // VALIDATION + const [allTemplates, viewerTeam, viewer] = await Promise.all([ + dataLoader.get('meetingTemplatesByType').load({meetingType: 'poker', teamId}), + dataLoader.get('teams').load(teamId), + dataLoader.get('users').loadNonNull(viewerId) + ]) + + if (!viewerTeam) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + if (getFeatureTier(viewerTeam) === 'starter' && viewer.freeCustomPokerTemplatesRemaining === 0) { + return standardError(new Error('You have reached the limit of free custom templates.'), { + userId: viewerId + }) + } else { + decrementFreeTemplatesRemaining(viewerId, 'poker') + viewer.freeCustomPokerTemplatesRemaining = viewer.freeCustomPokerTemplatesRemaining - 1 + } + let data + if (parentTemplateId) { + const parentTemplate = await dataLoader.get('meetingTemplates').load(parentTemplateId) + if (!parentTemplate) { + return standardError(new Error('Parent template not found'), {userId: viewerId}) + } + const {name, scope} = parentTemplate + if (scope === 'TEAM') { + if (!isTeamMember(authToken, parentTemplate.teamId)) + return standardError(new Error('Template is scoped to team'), {userId: viewerId}) + } else if (scope === 'ORGANIZATION') { + const parentTemplateTeam = await dataLoader.get('teams').load(parentTemplate.teamId) + const isInOrg = + parentTemplateTeam && + (await isUserInOrg(getUserId(authToken), parentTemplateTeam?.orgId, dataLoader)) + if (!isInOrg) { + return standardError(new Error('Template is scoped to organization'), {userId: viewerId}) + } + } + const copyName = `${name} Copy` + const existingCopyCount = allTemplates.filter((template) => + template.name.startsWith(copyName) + ).length + const newName = existingCopyCount === 0 ? copyName : `${copyName} #${existingCopyCount + 1}` + const newTemplate = new PokerTemplate({ + name: newName, + teamId, + orgId: viewerTeam.orgId, + parentTemplateId, + mainCategory: parentTemplate.mainCategory, + illustrationUrl: parentTemplate.illustrationUrl + }) + + const dimensions = await dataLoader + .get('templateDimensionsByTemplateId') + .load(parentTemplate.id) + const activeDimensions = dimensions.filter(({removedAt}: TemplateDimension) => !removedAt) + const newTemplateDimensions = activeDimensions.map((dimension: TemplateDimension) => { + return new TemplateDimension({ + ...dimension, + teamId, + templateId: newTemplate.id + }) + }) + + await Promise.all([ + r.table('TemplateDimension').insert(newTemplateDimensions).run(), + insertMeetingTemplate(newTemplate), + decrementFreeTemplatesRemaining(viewerId, 'poker') + ]) + viewer.freeCustomPokerTemplatesRemaining = viewer.freeCustomPokerTemplatesRemaining - 1 + analytics.templateMetrics(viewer, newTemplate, 'Template Cloned') + data = {templateId: newTemplate.id} + } else { + const {orgId} = viewerTeam + + const templateCount = allTemplates.length + const newTemplate = new PokerTemplate({ + name: `*New Template #${templateCount + 1}`, + teamId, + orgId, + mainCategory: 'estimation', + illustrationUrl: getTemplateIllustrationUrl('estimatedEffortTemplate.png') + }) + const templateId = newTemplate.id + const newDimension = new TemplateDimension({ + scaleId: SprintPokerDefaults.DEFAULT_SCALE_ID, + description: '', + sortOrder: 0, + name: '*New Dimension', + teamId, + templateId + }) + + await Promise.all([ + r.table('TemplateDimension').insert(newDimension).run(), + insertMeetingTemplate(newTemplate), + decrementFreeTemplatesRemaining(viewerId, 'poker') + ]) + viewer.freeCustomPokerTemplatesRemaining = viewer.freeCustomPokerTemplatesRemaining - 1 + analytics.templateMetrics(viewer, newTemplate, 'Template Created') + data = {templateId} + } + publish(SubscriptionChannel.TEAM, teamId, 'AddPokerTemplateSuccess', data, subOptions) + return data +} + +export default addPokerTemplate diff --git a/packages/server/graphql/public/mutations/addReflectTemplate.ts b/packages/server/graphql/public/mutations/addReflectTemplate.ts new file mode 100644 index 00000000000..b2c5c7479a4 --- /dev/null +++ b/packages/server/graphql/public/mutations/addReflectTemplate.ts @@ -0,0 +1,128 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import insertMeetingTemplate from '../../../postgres/queries/insertMeetingTemplate' +import {getUserId, isTeamMember, isUserInOrg} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {analytics} from '../../../utils/analytics/analytics' +import {getFeatureTier} from '../../types/helpers/getFeatureTier' +import decrementFreeTemplatesRemaining from '../../../postgres/queries/decrementFreeTemplatesRemaining' +import {MutationResolvers} from '../resolverTypes' +import ReflectTemplate from '../../../database/types/ReflectTemplate' +import {PALETTE} from '../../../../client/styles/paletteV3' +import makeRetroTemplates from '../../mutations/helpers/makeRetroTemplates' +import RetrospectivePrompt from '../../../database/types/RetrospectivePrompt' + +const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( + _source, + {teamId, parentTemplateId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const viewerId = getUserId(authToken) + + // AUTH + if (!isTeamMember(authToken, teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // VALIDATION + const [allTemplates, viewerTeam, viewer] = await Promise.all([ + dataLoader.get('meetingTemplatesByType').load({meetingType: 'retrospective', teamId}), + dataLoader.get('teams').load(teamId), + dataLoader.get('users').loadNonNull(viewerId) + ]) + + if (!viewerTeam) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + if (getFeatureTier(viewerTeam) === 'starter' && viewer.freeCustomRetroTemplatesRemaining === 0) { + return standardError(new Error('You have reached the limit of free custom templates.'), { + userId: viewerId + }) + } + let data + if (parentTemplateId) { + const parentTemplate = await dataLoader.get('meetingTemplates').load(parentTemplateId) + if (!parentTemplate) { + return standardError(new Error('Parent template not found'), {userId: viewerId}) + } + const {name, scope} = parentTemplate + if (scope === 'TEAM') { + if (!isTeamMember(authToken, parentTemplate.teamId)) + return standardError(new Error('Template is scoped to team'), {userId: viewerId}) + } else if (scope === 'ORGANIZATION') { + const parentTemplateTeam = await dataLoader.get('teams').load(parentTemplate.teamId) + const isInOrg = + parentTemplateTeam && + (await isUserInOrg(getUserId(authToken), parentTemplateTeam?.orgId, dataLoader)) + if (!isInOrg) { + return standardError(new Error('Template is scoped to organization'), {userId: viewerId}) + } + } + const copyName = `${name} Copy` + const existingCopyCount = allTemplates.filter((template) => + template.name.startsWith(copyName) + ).length + const newName = existingCopyCount === 0 ? copyName : `${copyName} #${existingCopyCount + 1}` + const newTemplate = new ReflectTemplate({ + name: newName, + teamId, + orgId: viewerTeam.orgId, + parentTemplateId, + illustrationUrl: parentTemplate.illustrationUrl, + mainCategory: parentTemplate.mainCategory + }) + const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(parentTemplate.id) + const activePrompts = prompts.filter(({removedAt}: RetrospectivePrompt) => !removedAt) + const newTemplatePrompts = activePrompts.map((prompt: RetrospectivePrompt) => { + return new RetrospectivePrompt({ + ...prompt, + teamId, + templateId: newTemplate.id, + parentPromptId: prompt.id, + removedAt: null + }) + }) + + await Promise.all([ + r.table('ReflectPrompt').insert(newTemplatePrompts).run(), + insertMeetingTemplate(newTemplate), + decrementFreeTemplatesRemaining(viewerId, 'retro') + ]) + viewer.freeCustomRetroTemplatesRemaining = viewer.freeCustomRetroTemplatesRemaining - 1 + analytics.templateMetrics(viewer, newTemplate, 'Template Cloned') + data = {templateId: newTemplate.id} + } else { + const {orgId} = viewerTeam + // RESOLUTION + const templateCount = allTemplates.length + const base = { + [`*New Template #${templateCount + 1}`]: [ + { + question: 'New prompt', + description: '', + groupColor: PALETTE.JADE_400 + } + ] + } as const + const {reflectPrompts: newTemplatePrompts, templates} = makeRetroTemplates(teamId, orgId, base) + // guaranteed since base has 1 key + const newTemplate = templates[0]! + const {id: templateId} = newTemplate + await Promise.all([ + r.table('ReflectPrompt').insert(newTemplatePrompts).run(), + insertMeetingTemplate(newTemplate), + decrementFreeTemplatesRemaining(viewerId, 'retro') + ]) + viewer.freeCustomRetroTemplatesRemaining = viewer.freeCustomRetroTemplatesRemaining - 1 + analytics.templateMetrics(viewer, newTemplate, 'Template Created') + data = {templateId} + } + publish(SubscriptionChannel.TEAM, teamId, 'AddReflectTemplateSuccess', data, subOptions) + return data +} + +export default addPokerTemplate diff --git a/packages/server/graphql/public/mutations/updateMeetingTemplate.ts b/packages/server/graphql/public/mutations/updateMeetingTemplate.ts new file mode 100644 index 00000000000..a0be265f8de --- /dev/null +++ b/packages/server/graphql/public/mutations/updateMeetingTemplate.ts @@ -0,0 +1,49 @@ +import {SubscriptionChannel} from '../../../../client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import getPhase from '../../../utils/getPhase' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const updateMeetingTemplate: MutationResolvers['updateMeetingTemplate'] = async ( + _source, + {meetingId, templateId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const viewerId = getUserId(authToken) + const r = await getRethink() + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingRetrospective + if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) + if (!isTeamMember(authToken, meeting.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + const reflections = await dataLoader.get('retroReflectionsByMeetingId').load(meetingId) + if (reflections.length > 0) { + return standardError(new Error('Cannot change template after reflections have been created'), { + userId: viewerId + }) + } + const reflectPhase = getPhase(meeting.phases, 'reflect') + const hasCompletedReflectPhase = reflectPhase.stages.every((stage) => stage.isComplete) + if (hasCompletedReflectPhase) { + return standardError( + new Error('Cannot change template after reflection phase has been completed'), + { + userId: viewerId + } + ) + } + + await r.table('NewMeeting').get(meetingId).update({templateId}).run() + meeting.templateId = templateId + + const data = {meetingId, templateId} + publish(SubscriptionChannel.MEETING, meetingId, 'UpdateMeetingTemplateSuccess', data, subOptions) + return data +} + +export default updateMeetingTemplate diff --git a/packages/server/graphql/public/typeDefs/AddPokerTemplate.graphql b/packages/server/graphql/public/typeDefs/AddPokerTemplate.graphql new file mode 100644 index 00000000000..e9c6926d8eb --- /dev/null +++ b/packages/server/graphql/public/typeDefs/AddPokerTemplate.graphql @@ -0,0 +1,31 @@ +extend type Mutation { + """ + Adds a new poker template with a default dimension created. + """ + addPokerTemplate( + """ + The ID of the parent template, if this is a clone operation. + """ + parentTemplateId: ID + """ + The ID of the team for which the template is being created. + """ + teamId: ID! + ): AddPokerTemplatePayload! +} + +""" +Return value for addPokerTemplate, which could be an error +""" +union AddPokerTemplatePayload = ErrorPayload | AddPokerTemplateSuccess + +type AddPokerTemplateSuccess { + """ + The poker template that was created + """ + pokerTemplate: PokerTemplate! + """ + The user that created the template + """ + user: User! +} diff --git a/packages/server/graphql/public/typeDefs/AddReflectTemplate.graphql b/packages/server/graphql/public/typeDefs/AddReflectTemplate.graphql new file mode 100644 index 00000000000..ec7e1660e91 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/AddReflectTemplate.graphql @@ -0,0 +1,31 @@ +extend type Mutation { + """ + Adds a new reflect template with a default dimension created. + """ + addReflectTemplate( + """ + The ID of the parent template, if this is a clone operation. + """ + parentTemplateId: ID + """ + The ID of the team for which the template is being created. + """ + teamId: ID! + ): AddReflectTemplatePayload! +} + +""" +Return value for addReflectTemplate, which could be an error +""" +union AddReflectTemplatePayload = ErrorPayload | AddReflectTemplateSuccess + +type AddReflectTemplateSuccess { + """ + The reflect template that was created + """ + reflectTemplate: ReflectTemplate! + """ + The user that created the template + """ + user: User! +} diff --git a/packages/server/graphql/public/typeDefs/Organization.graphql b/packages/server/graphql/public/typeDefs/Organization.graphql index 485adcac9f6..40fe0800f8a 100644 --- a/packages/server/graphql/public/typeDefs/Organization.graphql +++ b/packages/server/graphql/public/typeDefs/Organization.graphql @@ -183,7 +183,6 @@ The types of flags that give an individual organization super powers """ type OrganizationFeatureFlags { noAISummary: Boolean! - AIGeneratedDiscussionPrompt: Boolean! standupAISummary: Boolean! noPromptToJoinOrg: Boolean! suggestGroups: Boolean! diff --git a/packages/server/graphql/public/typeDefs/Subscriptions.graphql b/packages/server/graphql/public/typeDefs/Subscriptions.graphql index 8e48da50750..3f2e1a48de5 100644 --- a/packages/server/graphql/public/typeDefs/Subscriptions.graphql +++ b/packages/server/graphql/public/typeDefs/Subscriptions.graphql @@ -54,6 +54,7 @@ type MeetingSubscriptionPayload { SetPokerSpectateSuccess: SetPokerSpectateSuccess SetTaskEstimateSuccess: SetTaskEstimateSuccess UpsertTeamPromptResponseSuccess: UpsertTeamPromptResponseSuccess + UpdateMeetingTemplateSuccess: UpdateMeetingTemplateSuccess } type NotificationSubscriptionPayload { @@ -151,8 +152,8 @@ type TeamSubscriptionPayload { UpdateTeamNamePayload: UpdateTeamNamePayload OldUpgradeToTeamTierPayload: OldUpgradeToTeamTierPayload UpgradeToTeamTierSuccess: UpgradeToTeamTierSuccess - AddReflectTemplatePayload: AddReflectTemplatePayload - AddPokerTemplatePayload: AddPokerTemplatePayload + AddReflectTemplateSuccess: AddReflectTemplateSuccess + AddPokerTemplateSuccess: AddPokerTemplateSuccess AddReflectTemplatePromptPayload: AddReflectTemplatePromptPayload AddPokerTemplateDimensionPayload: AddPokerTemplateDimensionPayload AddPokerTemplateScalePayload: AddPokerTemplateScalePayload diff --git a/packages/server/graphql/public/typeDefs/User.graphql b/packages/server/graphql/public/typeDefs/User.graphql index 232c2027391..bd29eaf1c02 100644 --- a/packages/server/graphql/public/typeDefs/User.graphql +++ b/packages/server/graphql/public/typeDefs/User.graphql @@ -433,6 +433,10 @@ type User { The cursor, which is the templateId """ after: ID + """ + An optional argument to filter by template type + """ + type: MeetingTypeEnum ): MeetingTemplateConnection! """ @@ -458,4 +462,12 @@ type User { """ domain: String! ): ParseSAMLMetadataPayload! + """ + The number of free custom retro templates remaining + """ + freeCustomRetroTemplatesRemaining: Int! + """ + The number of free custom poker templates remaining + """ + freeCustomPokerTemplatesRemaining: Int! } diff --git a/packages/server/graphql/public/typeDefs/_legacy.graphql b/packages/server/graphql/public/typeDefs/_legacy.graphql index 75ba9203d1f..e0e09d93056 100644 --- a/packages/server/graphql/public/typeDefs/_legacy.graphql +++ b/packages/server/graphql/public/typeDefs/_legacy.graphql @@ -5168,11 +5168,6 @@ type Mutation { comment: AddCommentInput! ): AddCommentPayload! - """ - Add a new poker template with a default dimension created - """ - addPokerTemplate(parentTemplateId: ID, teamId: ID!): AddPokerTemplatePayload! - """ Add a new dimension for the poker template """ @@ -5191,11 +5186,6 @@ type Mutation { scaleValue: AddTemplateScaleInput! ): AddPokerTemplateScaleValuePayload! - """ - Add a new template full of prompts - """ - addReflectTemplate(parentTemplateId: ID, teamId: ID!): AddReflectTemplatePayload - """ Add a new template full of prompts """ @@ -6215,7 +6205,6 @@ type Mutation { pokerResetDimension(meetingId: ID!, stageId: ID!): PokerResetDimensionPayload! """ - """ pokerAnnounceDeckHover( meetingId: ID! @@ -6262,7 +6251,6 @@ type Mutation { ): SetPokerSpectatePayload! """ - """ persistGitHubSearchQuery( """ @@ -6497,11 +6485,6 @@ input AddCommentInput { threadParentId: ID } -type AddPokerTemplatePayload { - error: StandardMutationError - pokerTemplate: PokerTemplate -} - type AddPokerTemplateDimensionPayload { error: StandardMutationError dimension: TemplateDimension @@ -6546,11 +6529,6 @@ enum GcalVideoTypeEnum { zoom } -type AddReflectTemplatePayload { - error: StandardMutationError - reflectTemplate: ReflectTemplate -} - type AddReflectTemplatePromptPayload { error: StandardMutationError prompt: ReflectPrompt diff --git a/packages/server/graphql/public/typeDefs/updateFeatureFlag.graphql b/packages/server/graphql/public/typeDefs/updateFeatureFlag.graphql index ba9506cf4cd..411d2f484f6 100644 --- a/packages/server/graphql/public/typeDefs/updateFeatureFlag.graphql +++ b/packages/server/graphql/public/typeDefs/updateFeatureFlag.graphql @@ -3,7 +3,6 @@ A flag to give an individual user super powers """ enum UserFlagEnum { standups - azureDevOps insights recurrence noAISummary @@ -19,7 +18,6 @@ The types of flags that give an individual user super powers """ type UserFeatureFlags { standups: Boolean! - azureDevOps: Boolean! insights: Boolean! recurrence: Boolean! noAISummary: Boolean! diff --git a/packages/server/graphql/public/typeDefs/updateMeetingTemplate.graphql b/packages/server/graphql/public/typeDefs/updateMeetingTemplate.graphql new file mode 100644 index 00000000000..1f378e42dbe --- /dev/null +++ b/packages/server/graphql/public/typeDefs/updateMeetingTemplate.graphql @@ -0,0 +1,27 @@ +extend type Mutation { + """ + Update a meeting template + """ + updateMeetingTemplate( + """ + The id of the meeting + """ + meetingId: ID! + """ + The id of the meeting template + """ + templateId: ID! + ): UpdateMeetingTemplatePayload! +} + +""" +Return value for updateMeetingTemplate, which could be an error +""" +union UpdateMeetingTemplatePayload = ErrorPayload | UpdateMeetingTemplateSuccess + +type UpdateMeetingTemplateSuccess { + """ + The updated meeting + """ + meeting: NewMeeting! +} diff --git a/packages/server/graphql/public/types/AddPokerTemplateSuccess.ts b/packages/server/graphql/public/types/AddPokerTemplateSuccess.ts new file mode 100644 index 00000000000..f6d2b3d9146 --- /dev/null +++ b/packages/server/graphql/public/types/AddPokerTemplateSuccess.ts @@ -0,0 +1,18 @@ +import {getUserId} from '../../../utils/authorization' +import {AddPokerTemplateSuccessResolvers, PokerTemplate} from '../resolverTypes' + +export type AddPokerTemplateSuccessSource = { + templateId: string +} + +const AddPokerTemplateSuccess: AddPokerTemplateSuccessResolvers = { + pokerTemplate: async ({templateId}, _args, {dataLoader}) => { + return (await dataLoader.get('meetingTemplates').load(templateId)) as PokerTemplate + }, + user: async (_src, _args, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + return dataLoader.get('users').loadNonNull(viewerId) + } +} + +export default AddPokerTemplateSuccess diff --git a/packages/server/graphql/public/types/AddReflectTemplateSuccess.ts b/packages/server/graphql/public/types/AddReflectTemplateSuccess.ts new file mode 100644 index 00000000000..eff41da3848 --- /dev/null +++ b/packages/server/graphql/public/types/AddReflectTemplateSuccess.ts @@ -0,0 +1,18 @@ +import {getUserId} from '../../../utils/authorization' +import {AddReflectTemplateSuccessResolvers, ReflectTemplate} from '../resolverTypes' + +export type AddPokerTemplateSuccessSource = { + templateId: string +} + +const AddPokerTemplateSuccess: AddReflectTemplateSuccessResolvers = { + reflectTemplate: async ({templateId}, _args, {dataLoader}) => { + return (await dataLoader.get('meetingTemplates').load(templateId)) as ReflectTemplate + }, + user: async (_src, _args, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + return dataLoader.get('users').loadNonNull(viewerId) + } +} + +export default AddPokerTemplateSuccess diff --git a/packages/server/graphql/public/types/OrganizationFeatureFlags.ts b/packages/server/graphql/public/types/OrganizationFeatureFlags.ts index 14f4d59e6a4..9903dc8dd4e 100644 --- a/packages/server/graphql/public/types/OrganizationFeatureFlags.ts +++ b/packages/server/graphql/public/types/OrganizationFeatureFlags.ts @@ -4,7 +4,6 @@ const OrganizationFeatureFlags: OrganizationFeatureFlagsResolvers = { noAISummary: ({noAISummary}) => !!noAISummary, standupAISummary: ({standupAISummary}) => !!standupAISummary, noPromptToJoinOrg: ({noPromptToJoinOrg}) => !!noPromptToJoinOrg, - AIGeneratedDiscussionPrompt: ({AIGeneratedDiscussionPrompt}) => !!AIGeneratedDiscussionPrompt, zoomTranscription: ({zoomTranscription}) => !!zoomTranscription, shareSummary: ({shareSummary}) => !!shareSummary, suggestGroups: ({suggestGroups}) => !!suggestGroups, diff --git a/packages/server/graphql/public/types/UpdateMeetingTemplateSuccess.ts b/packages/server/graphql/public/types/UpdateMeetingTemplateSuccess.ts new file mode 100644 index 00000000000..33a52658893 --- /dev/null +++ b/packages/server/graphql/public/types/UpdateMeetingTemplateSuccess.ts @@ -0,0 +1,14 @@ +import {UpdateMeetingTemplateSuccessResolvers} from '../resolverTypes' + +export type UpdateMeetingTemplateSuccessSource = { + meetingId: string +} + +const UpdateMeetingTemplateSuccess: UpdateMeetingTemplateSuccessResolvers = { + meeting: async ({meetingId}, _args, {dataLoader}) => { + const meeting = await dataLoader.get('newMeetings').load(meetingId) + return meeting + } +} + +export default UpdateMeetingTemplateSuccess diff --git a/packages/server/graphql/public/types/User.ts b/packages/server/graphql/public/types/User.ts index cd396dd8963..f46afe30511 100644 --- a/packages/server/graphql/public/types/User.ts +++ b/packages/server/graphql/public/types/User.ts @@ -114,7 +114,7 @@ const User: UserResolvers = { const invoice = await manager.retrieveInvoice(invoiceId) return generateInvoice(invoice, stripeLineItems, orgId, invoiceId, dataLoader) }, - availableTemplates: async ({id: userId}, {first, after}, {authToken, dataLoader}) => { + availableTemplates: async ({id: userId}, {first, after, type}, {authToken, dataLoader}) => { const viewerId = getUserId(authToken) const user = await dataLoader.get('users').loadNonNull(userId) const teamIds = @@ -175,6 +175,7 @@ const User: UserResolvers = { ...activity, sortOrder: getScore(activity, teamIds) })) + .filter((activity) => !type || activity.type === type) .sort((a, b) => (a.sortOrder > b.sortOrder ? -1 : 1)) return connectionFromTemplateArray(allActivities, first, after) diff --git a/packages/server/graphql/public/types/UserFeatureFlags.ts b/packages/server/graphql/public/types/UserFeatureFlags.ts index 806a33b7c8a..f8c72854653 100644 --- a/packages/server/graphql/public/types/UserFeatureFlags.ts +++ b/packages/server/graphql/public/types/UserFeatureFlags.ts @@ -1,7 +1,6 @@ import {UserFeatureFlagsResolvers} from '../resolverTypes' const UserFeatureFlags: UserFeatureFlagsResolvers = { - azureDevOps: ({azureDevOps}) => !!azureDevOps, insights: ({insights}) => !!insights, noAISummary: ({noAISummary}) => !!noAISummary, noMeetingHistoryLimit: ({noMeetingHistoryLimit}) => !!noMeetingHistoryLimit, diff --git a/packages/server/graphql/queries/team.ts b/packages/server/graphql/queries/team.ts index 40af63c637d..45c55e50764 100644 --- a/packages/server/graphql/queries/team.ts +++ b/packages/server/graphql/queries/team.ts @@ -22,7 +22,13 @@ export default { {authToken, dataLoader}: GQLContext, {operation}: GraphQLResolveInfo ) { - if (!isTeamMember(authToken, teamId) && !isSuperUser(authToken)) { + const team = await dataLoader.get('teams').loadNonNull(teamId) + const {orgId} = team + const viewerId = getUserId(authToken) + const {role} = + (await dataLoader.get('organizationUsersByUserIdOrgId').load({userId: viewerId, orgId})) ?? {} + const isOrgAdmin = role === 'ORG_ADMIN' + if (!isOrgAdmin && !isTeamMember(authToken, teamId) && !isSuperUser(authToken)) { const viewerId = getUserId(authToken) if (!HANDLED_OPS.includes(operation?.name?.value ?? '')) { standardError(new Error('Team not found'), {userId: viewerId}) diff --git a/packages/server/graphql/rootMutation.ts b/packages/server/graphql/rootMutation.ts index e82930a04ca..1fd42b72e43 100644 --- a/packages/server/graphql/rootMutation.ts +++ b/packages/server/graphql/rootMutation.ts @@ -6,11 +6,9 @@ import addComment from './mutations/addComment' import addGitHubAuth from './mutations/addGitHubAuth' import addIntegrationProvider from './mutations/addIntegrationProvider' import addOrg from './mutations/addOrg' -import addPokerTemplate from './mutations/addPokerTemplate' import addPokerTemplateDimension from './mutations/addPokerTemplateDimension' import addPokerTemplateScale from './mutations/addPokerTemplateScale' import addPokerTemplateScaleValue from './mutations/addPokerTemplateScaleValue' -import addReflectTemplate from './mutations/addReflectTemplate' import addReflectTemplatePrompt from './mutations/addReflectTemplatePrompt' import addSlackAuth from './mutations/addSlackAuth' import addTeam from './mutations/addTeam' @@ -129,11 +127,9 @@ export default new GraphQLObjectType({ addAgendaItem, addAtlassianAuth, addComment, - addPokerTemplate, addPokerTemplateDimension, addPokerTemplateScale, addPokerTemplateScaleValue, - addReflectTemplate, addReflectTemplatePrompt, addSlackAuth, addGitHubAuth, diff --git a/packages/server/graphql/types/AddPokerTemplatePayload.ts b/packages/server/graphql/types/AddPokerTemplatePayload.ts deleted file mode 100644 index 2616e83a8ff..00000000000 --- a/packages/server/graphql/types/AddPokerTemplatePayload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import PokerTemplate from './PokerTemplate' -import StandardMutationError from './StandardMutationError' - -const AddPokerTemplatePayload = new GraphQLObjectType({ - name: 'AddPokerTemplatePayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - pokerTemplate: { - type: PokerTemplate, - resolve: ({templateId}, _args: unknown, {dataLoader}) => { - if (!templateId) return null - return dataLoader.get('meetingTemplates').load(templateId) - } - } - }) -}) - -export default AddPokerTemplatePayload diff --git a/packages/server/graphql/types/AddReflectTemplatePayload.ts b/packages/server/graphql/types/AddReflectTemplatePayload.ts deleted file mode 100644 index 3c6e4bcf5bf..00000000000 --- a/packages/server/graphql/types/AddReflectTemplatePayload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectTemplate from './ReflectTemplate' -import StandardMutationError from './StandardMutationError' - -const AddReflectTemplatePayload = new GraphQLObjectType({ - name: 'AddReflectTemplatePayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - reflectTemplate: { - type: ReflectTemplate, - resolve: ({templateId}, _args: unknown, {dataLoader}) => { - if (!templateId) return null - return dataLoader.get('meetingTemplates').load(templateId) - } - } - }) -}) - -export default AddReflectTemplatePayload diff --git a/packages/server/graphql/types/CreateAzureDevOpsAuthorizeUrlPayload.ts b/packages/server/graphql/types/CreateAzureDevOpsAuthorizeUrlPayload.ts deleted file mode 100644 index afd3caeb221..00000000000 --- a/packages/server/graphql/types/CreateAzureDevOpsAuthorizeUrlPayload.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {GraphQLObjectType, GraphQLString} from 'graphql' -import {GQLContext} from '../graphql' -import StandardMutationError from './StandardMutationError' - -export const CreateAzureDevOpsAuthorizeUrlPayload = new GraphQLObjectType({ - name: 'CreateAzureDevOpsAuthorizeUrlSuccess', - fields: () => ({ - error: { - type: StandardMutationError - }, - url: { - type: GraphQLString, - description: 'Authorization URL including oauth_token to start authorization flow' - } - }) -}) - -export default CreateAzureDevOpsAuthorizeUrlPayload diff --git a/packages/server/package.json b/packages/server/package.json index 25a503e3254..68cfcfbb846 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.22.3", + "version": "7.23.1", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -94,7 +94,7 @@ "cheerio": "^1.0.0-rc.10", "dataloader": "^2.0.0", "dd-trace": "^4.2.0", - "dotenv": "8.0.0", + "dotenv": "8.6.0", "dotenv-expand": "5.1.0", "draft-js": "https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6", "draft-js-export-markdown": "^1.3.3", @@ -124,7 +124,7 @@ "oauth-1.0a": "^2.2.6", "openai": "^4.24.1", "oy-vey": "^0.12.1", - "parabol-client": "7.22.3", + "parabol-client": "7.23.1", "pg": "^8.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/packages/server/postgres/migrations/1709933510512_addFreeCustomTemplatesRemaining.ts b/packages/server/postgres/migrations/1709933510512_addFreeCustomTemplatesRemaining.ts new file mode 100644 index 00000000000..fd2ceae8931 --- /dev/null +++ b/packages/server/postgres/migrations/1709933510512_addFreeCustomTemplatesRemaining.ts @@ -0,0 +1,30 @@ +import {Kysely, PostgresDialect} from 'kysely' +import getPg from '../getPg' + +export async function up() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + + await pg.schema + .alterTable('User') + .addColumn('freeCustomRetroTemplatesRemaining', 'int2', (col) => col.defaultTo(2).notNull()) + .addColumn('freeCustomPokerTemplatesRemaining', 'int2', (col) => col.defaultTo(2).notNull()) + .execute() +} + +export async function down() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + + await pg.schema + .alterTable('User') + .dropColumn('freeCustomRetroTemplatesRemaining') + .dropColumn('freeCustomPokerTemplatesRemaining') + .execute() +} diff --git a/packages/server/postgres/queries/decrementFreeTemplatesRemaining.ts b/packages/server/postgres/queries/decrementFreeTemplatesRemaining.ts new file mode 100644 index 00000000000..2eef866348b --- /dev/null +++ b/packages/server/postgres/queries/decrementFreeTemplatesRemaining.ts @@ -0,0 +1,18 @@ +import getKysely from '../getKysely' + +const decrementFreeTemplatesRemaining = async (userId: string, templateType: 'retro' | 'poker') => { + const pg = getKysely() + const customTemplateType = + templateType === 'retro' + ? 'freeCustomRetroTemplatesRemaining' + : 'freeCustomPokerTemplatesRemaining' + + await pg + .updateTable('User') + .set((eb) => ({[customTemplateType]: eb(customTemplateType, '-', 1)})) + .where('id', '=', userId) + .where(customTemplateType, '>', 0) + .executeTakeFirst() +} + +export default decrementFreeTemplatesRemaining diff --git a/packages/server/utils/AzureDevOpsServerManager.ts b/packages/server/utils/AzureDevOpsServerManager.ts index 7f495fe771b..ba3cf7f04f3 100644 --- a/packages/server/utils/AzureDevOpsServerManager.ts +++ b/packages/server/utils/AzureDevOpsServerManager.ts @@ -266,7 +266,7 @@ class AzureDevOpsServerManager implements TaskIntegrationManager { grant_type: 'authorization_code', code: code, code_verifier: codeVerifier, - redirect_uri: makeAppURL(appOrigin, 'auth/ado') + redirect_uri: makeAppURL(appOrigin, 'auth/ado2') }) } @@ -702,16 +702,14 @@ class AzureDevOpsServerManager implements TaskIntegrationManager { const body = { ...params, - client_id: this.provider.clientId + client_id: this.provider.clientId, + client_secret: this.provider.clientSecret } - const additonalHeaders = { - Origin: appOrigin - } const tenantId = this.provider.tenantId const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token` const contentType = 'application/x-www-form-urlencoded' - const oAuthRes = await authorizeOAuth2({authUrl, body, additonalHeaders, contentType}) + const oAuthRes = await authorizeOAuth2({authUrl, body, contentType}) if (!isError(oAuthRes)) { this.accessToken = oAuthRes.accessToken } diff --git a/yarn.lock b/yarn.lock index 74d28b2fb23..dc34fd16fae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10505,10 +10505,10 @@ core-js-pure@^3.8.1: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.20.3.tgz#6cc4f36da06c61d95254efc54024fe4797fd5d02" integrity sha512-Q2H6tQ5MtPtcC7f3HxJ48i4Q7T9ybPKgvWyuH7JXIoNa2pm0KuBnycsET/qw1SLLZYfbsbrZQNMeIOClb+6WIA== -core-js@3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.1.tgz#f51523668ac8a294d1285c3b9db44025fda66d47" - integrity sha512-9Id2xHY1W7m8hCl8NkhQn5CufmF/WuR30BTRewvCXc1aZd3kMECwNZ69ndLbekKfakw9Rf2Xyc+QR6E7Gg+obg== +core-js@3.36.0: + version "3.36.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.36.0.tgz#e752fa0b0b462a0787d56e9d73f80b0f7c0dde68" + integrity sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw== core-util-is@~1.0.0: version "1.0.3" @@ -11168,10 +11168,10 @@ dogapi@2.8.4: minimist "^1.2.5" rc "^1.2.8" -dom-confetti@~0.0.11: - version "0.0.15" - resolved "https://registry.yarnpkg.com/dom-confetti/-/dom-confetti-0.0.15.tgz#fd7dd6888da3dc6b54fd53326c8fa826ef25106b" - integrity sha512-KJKrmHcydwoS6bTD0wVM/L7LZz1rQNJA+Y3Zw9ZVYNpx5efqoVwIY2rqY/F2sEpJuGGCRff/o8bAmfQ8KO4Grw== +dom-confetti@0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-confetti/-/dom-confetti-0.2.2.tgz#bdf2e7652d37b5cffb532c0a3263d108dd8a2363" + integrity sha512-+UVH9Y85qmpTnbmFURwLWjqLIykyIrsNSRkPX/eFlBuOURz9RDX8JoZHnajZHyFuCV0w/K3+tZK0ztfoTw6ejg== dom-converter@^0.2.0: version "0.2.0" @@ -11269,6 +11269,11 @@ dotenv@8.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.0.0.tgz#ed310c165b4e8a97bb745b0a9d99c31bda566440" integrity sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg== +dotenv@8.6.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" + integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== + dotenv@^16.0.0, dotenv@^16.0.3: version "16.0.3" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" @@ -13043,10 +13048,10 @@ graphql-typed@^0.4.1: resolved "https://registry.yarnpkg.com/graphql-typed/-/graphql-typed-0.4.1.tgz#d612f1a8215fc5b14b51d5a88b06c0174d6a6e47" integrity sha512-6a4BHG/uXMkxY+UeqmsTrwCvTy3BbFAYlKsVC2MzvAISXh+FcbBaJEfXsbcZKrydCFnVD3Xlg74Esip5O3LyLA== -graphql-typed@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/graphql-typed/-/graphql-typed-0.6.1.tgz#a305bad26cc1e6a5f7cf34e720d9cbd7bc57317d" - integrity sha512-41J/CLton6F6tt5AGmKbroU5RSyXOSH5My2D396yXcNuQynaehP8FFG9bx/XjLukJMgLHJ8pUSHOw+fAqcGklw== +graphql-typed@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/graphql-typed/-/graphql-typed-0.7.2.tgz#c917eb79e58f340a842b204212e29506db481c2c" + integrity sha512-Np+YLzDCTY92ptXN5Rxi34fMJ4dy/vx0jZWnH0IVjpLW5SSoirve4ymIBy1UbkS//uEMCdNtvX4Y6loOJ3vyvA== graphql-ws@^5.14.0: version "5.14.0" @@ -18316,12 +18321,12 @@ react-day-picker@^8.3.7: resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.3.7.tgz#35de1325376984e4dbaec1fed6888f45dc46ad1d" integrity sha512-sQvyJde6OKsXIB0ZFwUBmaDNb4IyKypv/uSyauQ6AUw4F6vmPijJsQNEVsxrxXQ4L3GZKIpcCD/7Pftz/sLegA== -react-dom-confetti@^0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/react-dom-confetti/-/react-dom-confetti-0.0.10.tgz#6cde7af4af974ecf98ddf9da95c938144c68a4f0" - integrity sha512-EJMUmD9Z3/87wIjyQic5ZGd2beHE9K50vxouLdV+yPcBitlsVznBXKX900xEbzQ/DOMt+TycEg2CvB/V+pwJtQ== +react-dom-confetti@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/react-dom-confetti/-/react-dom-confetti-0.2.0.tgz#76df26762da532057d5b1fbe38a8096f9dc33d40" + integrity sha512-+XRTi+WlCrcRN2dTjdEopOaPFtS7hpaHRRQ0sHiVRGqpchKz4QVh3i+6eLEEpNHYpN2VgPmhjvJ/vnjmUYhlIQ== dependencies: - dom-confetti "~0.0.11" + dom-confetti "0.2.2" react-dom@^17.0.2: version "17.0.2" @@ -18451,15 +18456,7 @@ react-style-singleton@^2.2.1: invariant "^2.2.4" tslib "^2.0.0" -react-swipeable-views-core@0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.13.1.tgz#8829a922462a8bdd701709cd1b385393d38f1527" - integrity sha512-EP8sCvvD7VDiZLglPt9icMuMNu8qLRLk0ab/fB1HXv7lX8ClnwF3UMCM0ZrN3sguSY7CsX3LevducGGsT1VcDg== - dependencies: - "@babel/runtime" "7.0.0" - warning "^4.0.1" - -react-swipeable-views-core@^0.14.0, react-swipeable-views-core@^0.14.0-alpha.0: +react-swipeable-views-core@0.14.0, react-swipeable-views-core@^0.14.0, react-swipeable-views-core@^0.14.0-alpha.0: version "0.14.0" resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.14.0.tgz#6ac443a7cc7bc5ea022fbd549292bb5fff361cce" integrity sha512-0W/e9uPweNEOSPjmYtuKSC/SvKKg1sfo+WtPdnxeLF3t2L82h7jjszuOHz9C23fzkvLfdgkaOmcbAxE9w2GEjA== From fec143b773953c376e9766c2ed822ecbccb910e4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 16:12:39 +0200 Subject: [PATCH 09/19] chore(release): Test v7.24.1 (#9587) Signed-off-by: dependabot[bot] Signed-off-by: Matt Krick Co-authored-by: Rafa <101704572+rafaelromcar-parabol@users.noreply.github.com> Co-authored-by: Georg Bremer Co-authored-by: Nick O'Ferrall Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> Co-authored-by: Matt Krick Co-authored-by: Jordan Husney Co-authored-by: adaniels-parabol <71724289+adaniels-parabol@users.noreply.github.com> Co-authored-by: snyk-bot Co-authored-by: Dale Bumblis <135627447+dbumblis-parabol@users.noreply.github.com> Co-authored-by: Bartosz Jarocki Co-authored-by: Marcus Wermuth Co-authored-by: Rafael Romero Co-authored-by: Bruce Tian Co-authored-by: github-actions --- .env.example | 2 +- .github/workflows/build.yml | 4 +- .github/workflows/test.yml | 14 +- .release-please-manifest.json | 2 +- CHANGELOG.md | 21 + docker/images/parabol-ubi/README.md | 24 +- .../images/parabol-ubi/environments/pipeline | 4 + docker/stacks/development/docker-compose.yml | 2 +- ...docker-compose.yaml => docker-compose.yml} | 2 +- package.json | 11 +- packages/chronos/package.json | 4 +- packages/chronos/tsconfig.json | 11 +- packages/client/components/AnimatedFade.tsx | 74 -- .../AzureDevOpsScopingSelectAllIssues.tsx | 6 +- packages/client/hooks/useAtlassianSites.ts | 43 - .../components/MeetingSummaryEmailRootSSR.tsx | 1 - .../NotificationSummaryEmailRoot.tsx | 2 +- .../MeetingSummaryEmail.tsx | 14 - .../handlers/handleUpdateTeamMembers.ts | 11 +- packages/client/package.json | 10 +- packages/client/serviceWorker/sw.ts | 6 +- packages/client/serviceWorker/tsconfig.json | 4 +- .../client/shared/gqlIds/EmbedderChannelId.ts | 9 + packages/client/tsconfig.json | 9 +- packages/client/types/generics.ts | 3 + packages/client/types/modules.d.ts | 1 + packages/client/types/reactHTML4.d.ts | 15 + .../ui/AlertDialog/AlertDialogContent.tsx | 4 +- .../ui/AlertDialog/AlertDialogPortal.tsx | 2 +- .../ui/AlertDialog/AlertDialogTrigger.tsx | 2 +- .../__tests__/parseEmailAddressList.test.ts | 7 +- ...etBezierTimePercentGivenDistancePercent.ts | 11 +- packages/client/utils/screenBugs/Bug.ts | 20 +- .../client/utils/screenBugs/BugController.ts | 22 +- packages/embedder/EMBEDDER_JOB_PRIORITY.ts | 6 + packages/embedder/EmbeddingsJobQueueStream.ts | 73 ++ packages/embedder/README.md | 41 +- packages/embedder/addEmbeddingsMetadata.ts | 15 + ...MetadataForRetrospectiveDiscussionTopic.ts | 144 +++ .../ai_models/AbstractEmbeddingsModel.ts | 154 +++ .../ai_models/AbstractGenerationModel.ts | 25 + packages/embedder/ai_models/AbstractModel.ts | 66 +- packages/embedder/ai_models/ModelManager.ts | 196 ++-- .../embedder/ai_models/OpenAIGeneration.ts | 32 +- .../ai_models/TextEmbeddingsInference.ts | 102 +- .../ai_models/TextGenerationInference.ts | 27 +- packages/embedder/custom.d.ts | 11 + packages/embedder/embedder.ts | 310 ++----- packages/embedder/establishPrimaryEmbedder.ts | 17 + packages/embedder/importHistoricalMetadata.ts | 16 + ...tHistoricalRetrospectiveDiscussionTopic.ts | 37 + packages/embedder/indexing/countWords.ts | 17 - .../indexing/createEmbeddingTextFrom.ts | 16 +- .../embedder/indexing/embeddingsTablesOps.ts | 198 ---- packages/embedder/indexing/failJob.ts | 17 + packages/embedder/indexing/getRedisClient.ts | 11 - .../embedder/indexing/getRootDataLoader.ts | 10 - .../indexing/retrospectiveDiscussionTopic.ts | 224 +---- packages/embedder/iso6393To1.ts | 195 ++++ packages/embedder/logMemoryUse.ts | 10 + packages/embedder/mergeAsyncIterators.ts | 96 ++ packages/embedder/package.json | 11 +- packages/embedder/processJob.ts | 13 + packages/embedder/processJobEmbed.ts | 101 ++ packages/embedder/resetStalledJobs.ts | 18 + packages/embedder/textEmbeddingsnterface.d.ts | 877 ++++++++++++++++++ packages/embedder/tsconfig.json | 11 +- packages/embedder/types/modules.d.ts | 4 + packages/embedder/types/shared.d.ts | 1 + packages/gql-executor/RedisStream.ts | 9 +- packages/gql-executor/gqlExecutor.ts | 7 +- packages/gql-executor/modules.d.ts | 2 + packages/gql-executor/package.json | 6 +- packages/gql-executor/tsconfig.json | 11 +- packages/integration-tests/package.json | 4 +- packages/server/__tests__/autoJoin.test.ts | 4 +- packages/server/__tests__/common.ts | 2 +- .../server/__tests__/disableAnonymity.test.ts | 14 +- packages/server/__tests__/loginSAML.test.ts | 24 - .../__tests__/processRecurrence.test.ts | 11 +- .../__tests__/startRetrospective.test.ts | 18 +- packages/server/database/rethinkDriver.ts | 2 +- .../__tests__/isCompanyDomain.test.ts | 2 - .../__tests__/isOrgVerified.test.ts | 39 +- .../__tests__/usersCustomRedisQueries.test.ts | 14 +- .../server/dataloader/customLoaderMakers.ts | 2 +- .../rethinkForeignKeyLoaderMakers.ts | 13 + packages/server/email/inlineImages.ts | 6 +- packages/server/generateUID.ts | 2 +- packages/server/graphql/executeGraphQL.ts | 19 +- .../mutations/helpers/publishToEmbedder.ts | 14 + .../mutations/helpers/safeEndRetrospective.ts | 16 +- .../private/mutations/runScheduledJobs.ts | 2 - packages/server/graphql/public/rootSchema.ts | 1 - .../gitlab/GitLabServerManager.ts | 2 +- packages/server/package.json | 7 +- .../1703031300000_addEmbeddingTables.ts | 6 +- .../1709934935000_embeddingsMetadataId.ts | 62 ++ .../types/{Meeting.ts => Meeting.d.ts} | 0 packages/server/tsconfig.json | 20 +- packages/server/types/custom.d.ts | 29 + packages/server/types/modules.d.ts | 1 + .../isRequestToJoinDomainAllowed.test.ts | 65 +- .../rateLimiters/InMemoryRateLimiter.test.ts | 2 +- packages/server/utils/analytics/analytics.ts | 11 +- packages/server/utils/getGitHubRequest.ts | 2 +- packages/server/utils/getGraphQLExecutor.ts | 2 +- packages/server/utils/uwsGetHeaders.ts | 2 +- release-please-config.json | 5 + scripts/webpack/utils/getProjectRoot.d.ts | 2 + tsconfig.base.json | 1 + yarn.lock | 398 ++++---- 112 files changed, 2802 insertions(+), 1538 deletions(-) rename docker/stacks/single-tenant-host/{docker-compose.yaml => docker-compose.yml} (98%) delete mode 100644 packages/client/components/AnimatedFade.tsx delete mode 100644 packages/client/hooks/useAtlassianSites.ts create mode 100644 packages/client/shared/gqlIds/EmbedderChannelId.ts create mode 100644 packages/client/types/reactHTML4.d.ts create mode 100644 packages/embedder/EMBEDDER_JOB_PRIORITY.ts create mode 100644 packages/embedder/EmbeddingsJobQueueStream.ts create mode 100644 packages/embedder/addEmbeddingsMetadata.ts create mode 100644 packages/embedder/addEmbeddingsMetadataForRetrospectiveDiscussionTopic.ts create mode 100644 packages/embedder/ai_models/AbstractEmbeddingsModel.ts create mode 100644 packages/embedder/ai_models/AbstractGenerationModel.ts create mode 100644 packages/embedder/custom.d.ts create mode 100644 packages/embedder/establishPrimaryEmbedder.ts create mode 100644 packages/embedder/importHistoricalMetadata.ts create mode 100644 packages/embedder/importHistoricalRetrospectiveDiscussionTopic.ts delete mode 100644 packages/embedder/indexing/countWords.ts delete mode 100644 packages/embedder/indexing/embeddingsTablesOps.ts create mode 100644 packages/embedder/indexing/failJob.ts delete mode 100644 packages/embedder/indexing/getRedisClient.ts delete mode 100644 packages/embedder/indexing/getRootDataLoader.ts create mode 100644 packages/embedder/iso6393To1.ts create mode 100644 packages/embedder/logMemoryUse.ts create mode 100644 packages/embedder/mergeAsyncIterators.ts create mode 100644 packages/embedder/processJob.ts create mode 100644 packages/embedder/processJobEmbed.ts create mode 100644 packages/embedder/resetStalledJobs.ts create mode 100644 packages/embedder/textEmbeddingsnterface.d.ts create mode 100644 packages/embedder/types/modules.d.ts create mode 100644 packages/embedder/types/shared.d.ts create mode 100644 packages/gql-executor/modules.d.ts create mode 100644 packages/server/graphql/mutations/helpers/publishToEmbedder.ts create mode 100644 packages/server/postgres/migrations/1709934935000_embeddingsMetadataId.ts rename packages/server/postgres/types/{Meeting.ts => Meeting.d.ts} (100%) create mode 100644 scripts/webpack/utils/getProjectRoot.d.ts diff --git a/.env.example b/.env.example index 2aa11780748..5f0285650a9 100644 --- a/.env.example +++ b/.env.example @@ -14,7 +14,7 @@ SOCKET_PORT='3001' # AI MODELS AI_EMBEDDING_MODELS='[{"model": "text-embeddings-inference:llmrails/ember-v1", "url": "http://localhost:3040/"}]' AI_GENERATION_MODELS='[{"model": "text-generation-inference:TheBloke/zephyr-7b-beta", "url": "http://localhost:3050/"}]' -AI_EMBEDDER_ENABLED='true' +AI_EMBEDDER_WORKERS='1' # APPLICATION # AMPLITUDE_WRITE_KEY='key_AMPLITUDE_WRITE_KEY' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 709f0a2d818..4da61b03eac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: id-token: "write" services: postgres: - image: pgvector/pgvector:pg15 + image: pgvector/pgvector:0.6.2-pg15 # This env variables must be the same in the file PARABOL_BUILD_ENV_PATH env: POSTGRES_PASSWORD: "temppassword" @@ -143,6 +143,6 @@ jobs: uses: ravsamhq/notify-slack-action@v2 with: status: ${{ job.status }} - notify_when: 'failure' + notify_when: "failure" env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_GH_ACTIONS_NOTIFICATIONS }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8378696b622..171e080964f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: id-token: "write" services: postgres: - image: postgres:15.4 + image: pgvector/pgvector:0.6.2-pg15 # This env variables must be the same in the file PARABOL_BUILD_ENV_PATH env: POSTGRES_PASSWORD: "temppassword" @@ -78,7 +78,6 @@ jobs: yarn db:migrate yarn pg:migrate up yarn pg:build - yarn pg:generate - name: Build for testing run: yarn build @@ -86,9 +85,6 @@ jobs: - name: Verify source is clean run: git diff --quiet HEAD || (echo "Changes in generated files detected"; git diff; exit 1) - - name: Check Code Quality - run: yarn codecheck - - name: Run Predeploy for Testing run: yarn predeploy @@ -100,6 +96,12 @@ jobs: wait-on: | http://localhost:3000/graphql + - name: Kysely Codegen + run: yarn pg:generate + + - name: Check Code Quality + run: yarn codecheck + - name: Run server tests run: yarn test:server -- --reporters=default --reporters=jest-junit env: @@ -139,6 +141,6 @@ jobs: uses: ravsamhq/notify-slack-action@v2 with: status: ${{ job.status }} - notify_when: 'failure' + notify_when: "failure" env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_GH_ACTIONS_NOTIFICATIONS }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json index bae38c98366..2fb3566ccf3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.23.1" + ".": "7.24.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 294dbfc8830..b5ed33f4256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.24.1](https://github.com/ParabolInc/parabol/compare/v7.24.0...v7.24.1) (2024-04-02) + + +### Fixed + +* embedder doesn't dive deep into schema ([#9582](https://github.com/ParabolInc/parabol/issues/9582)) ([8cdd901](https://github.com/ParabolInc/parabol/commit/8cdd9014c3277905605c6544de92d9ac2833a6e9)) +* embedder errors in embed length ([#9584](https://github.com/ParabolInc/parabol/issues/9584)) ([341b4b7](https://github.com/ParabolInc/parabol/commit/341b4b797ec6444066244f25916803e64c03258c)) +* Fetch CORS resources from network ([#9586](https://github.com/ParabolInc/parabol/issues/9586)) ([b6ddfa5](https://github.com/ParabolInc/parabol/commit/b6ddfa5755394633e83dadd0178234ef740454ea)) + +## [7.24.0](https://github.com/ParabolInc/parabol/compare/v7.23.1...v7.24.0) (2024-03-29) + + +### Added + +* prepare embedder for Production ([#9517](https://github.com/ParabolInc/parabol/issues/9517)) ([538c95c](https://github.com/ParabolInc/parabol/commit/538c95ce4dc7d4839b3e813006cb20e1b7d1d1c8)) + + +### Changed + +* fix tsconfig problems ([#9579](https://github.com/ParabolInc/parabol/issues/9579)) ([d1af0f1](https://github.com/ParabolInc/parabol/commit/d1af0f164c629e8fc075278cd63475e8913f4295)) + ## [7.23.1](https://github.com/ParabolInc/parabol/compare/v7.23.0...v7.23.1) (2024-03-28) diff --git a/docker/images/parabol-ubi/README.md b/docker/images/parabol-ubi/README.md index 920c6d48fd8..518269140bc 100644 --- a/docker/images/parabol-ubi/README.md +++ b/docker/images/parabol-ubi/README.md @@ -16,21 +16,21 @@ Recommended: ## Variables -| Name | Description | Possible values | Recommended value | -| -------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------- | -| `postgresql_tag` | PostgreSQL version from the [Docker image](https://hub.docker.com/_/postgres) | `Any tag` | `15.4` | -| `rethinkdb_tag` | RethinkDB version from the [Docker image](https://hub.docker.com/_/rethinkdb) | `Any tag` | `2.4.2` | -| `redis_tag` | Redis version from the [Docker image](https://hub.docker.com/_/redis) | `Any tag` | `7.0-alpine` | -| `_BUILD_ENV_PATH` | File `.env` used by the application during the build process | `Relative path from the root level of the repository` | `docker/parabol-ubi/environments/basic-env` | -| `_NODE_VERSION` | Node version, used by Docker to use the Docker image node:\_NODE_VERSION as base image to build | `Same as in root package.json` | | -| `_DOCKERFILE` | Dockerfile used to build the image | `Relative path from the root level of the repository` | `./docker/parabol-ubi/dockerfiles/basic.dockerfile` | -| `_DOCKER_REPOSITORY` | The destination repository | `String` | `parabol` | -| `_DOCKER_TAG` | Tag for the produced image | `String` | | +| Name | Description | Possible values | Recommended value | +| -------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------- | +| `postgresql_tag` | PostgreSQL version from the [Docker image](https://hub.docker.com/r/pgvector/pgvector) | `Any tag` | `0.6.2-pg15` | +| `rethinkdb_tag` | RethinkDB version from the [Docker image](https://hub.docker.com/_/rethinkdb) | `Any tag` | `2.4.2` | +| `redis_tag` | Redis version from the [Docker image](https://hub.docker.com/_/redis) | `Any tag` | `7.0-alpine` | +| `_BUILD_ENV_PATH` | File `.env` used by the application during the build process | `Relative path from the root level of the repository` | `docker/parabol-ubi/environments/basic-env` | +| `_NODE_VERSION` | Node version, used by Docker to use the Docker image node:\_NODE_VERSION as base image to build | `Same as in root package.json` | | +| `_DOCKERFILE` | Dockerfile used to build the image | `Relative path from the root level of the repository` | `./docker/parabol-ubi/dockerfiles/basic.dockerfile` | +| `_DOCKER_REPOSITORY` | The destination repository | `String` | `parabol` | +| `_DOCKER_TAG` | Tag for the produced image | `String` | | Example of variables: ```commandLine -export postgresql_tag=15.4; \ +export postgresql_tag=0.6.2-pg15; \ export rethinkdb_tag=2.4.2; \ export redis_tag=7.0-alpine; \ export _BUILD_ENV_PATH=docker/parabol-ubi/environments/basic-env; \ @@ -61,7 +61,7 @@ cp $_BUILD_ENV_PATH ./.env > :warning: Stop all database containers you might have running before executing the following command. If other database containers are running, some ports might be already taken. ```commandLine -docker run --name temp-postgres -e POSTGRES_PASSWORD=temppassword -e POSTGRES_USER=tempuser -e POSTGRES_DB=tempdb -d -p 5432:5432 postgres:$postgresql_tag && \ +docker run --name temp-postgres -e POSTGRES_PASSWORD=temppassword -e POSTGRES_USER=tempuser -e POSTGRES_DB=tempdb -d -p 5432:5432 pgvector/pgvector:$postgresql_tag && \ docker run --name temp-rethinkdb -d -p 28015:28015 -p 29015:29015 -p 8080:8080 rethinkdb:$rethinkdb_tag && \ docker run --name temp-redis -d -p 6379:6379 redis:$redis_tag ``` diff --git a/docker/images/parabol-ubi/environments/pipeline b/docker/images/parabol-ubi/environments/pipeline index cfc707c746b..cd111fab88b 100644 --- a/docker/images/parabol-ubi/environments/pipeline +++ b/docker/images/parabol-ubi/environments/pipeline @@ -54,3 +54,7 @@ STRIPE_PUBLISHABLE_KEY='pk_test_MNoKbCzQX0lhktuxxI7M14wd' STRIPE_SECRET_KEY='' STRIPE_WEBHOOK_SECRET='' HUBSPOT_API_KEY='' +AI_EMBEDDING_MODELS='[{"model": "text-embeddings-inference:llmrails/ember-v1", "url": "http://localhost:3040/"}]' +AI_GENERATION_MODELS='[{"model": "text-generation-inference:TheBloke/zephyr-7b-beta", "url": "http://localhost:3050/"}]' +AI_EMBEDDER_WORKERS='1' +POSTGRES_USE_PGVECTOR='true' diff --git a/docker/stacks/development/docker-compose.yml b/docker/stacks/development/docker-compose.yml index 91adbbb6578..a4509d051b9 100644 --- a/docker/stacks/development/docker-compose.yml +++ b/docker/stacks/development/docker-compose.yml @@ -70,7 +70,7 @@ services: networks: parabol-network: text-embeddings-inference: - image: ghcr.io/huggingface/text-embeddings-inference:cpu-0.6 + image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.2 command: - "--model-id=llmrails/ember-v1" platform: linux/x86_64 diff --git a/docker/stacks/single-tenant-host/docker-compose.yaml b/docker/stacks/single-tenant-host/docker-compose.yml similarity index 98% rename from docker/stacks/single-tenant-host/docker-compose.yaml rename to docker/stacks/single-tenant-host/docker-compose.yml index e5f662ed74a..a336201bfe3 100644 --- a/docker/stacks/single-tenant-host/docker-compose.yaml +++ b/docker/stacks/single-tenant-host/docker-compose.yml @@ -17,7 +17,7 @@ services: postgres: container_name: postgres profiles: ["databases"] - image: postgres:15.4 + image: pgvector/pgvector:0.6.2-pg15 restart: always env_file: .env environment: diff --git a/package.json b/package.json index 82179da713f..a6b2e22b178 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.23.1", + "version": "7.24.1", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -92,18 +92,19 @@ "@types/dotenv": "^6.1.1", "@types/jscodeshift": "^0.11.3", "@types/lodash.toarray": "^4.4.7", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", + "@typescript-eslint/eslint-plugin": "^7.4.0", + "@typescript-eslint/parser": "^7.4.0", "autoprefixer": "^10.4.13", "babel-loader": "^9.1.2", "concurrently": "^8.0.1", "copy-webpack-plugin": "^11.0.0", - "eslint-config-prettier": "^8.5.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", "graphql": "15.7.2", "html-webpack-plugin": "^5.5.0", "husky": "^7.0.4", "jscodeshift": "^0.14.0", - "kysely": "^0.27.2", + "kysely": "^0.27.3", "kysely-codegen": "^0.11.0", "lerna": "^6.4.1", "mini-css-extract-plugin": "^2.7.2", diff --git a/packages/chronos/package.json b/packages/chronos/package.json index b2e5061c6bc..b8e0b4886a7 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.23.1", + "version": "7.24.1", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -25,6 +25,6 @@ }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.23.1" + "parabol-server": "7.24.1" } } diff --git a/packages/chronos/tsconfig.json b/packages/chronos/tsconfig.json index 00f66831a3b..87e7ef08687 100644 --- a/packages/chronos/tsconfig.json +++ b/packages/chronos/tsconfig.json @@ -9,13 +9,6 @@ "~/*": ["client/*"] }, "outDir": "lib", - "lib": ["esnext"], - "types": ["node"] - }, - "files": [ - "chronos.ts", - "../server/types/webpackEnv.ts", - "../server/types/modules.d.ts", - "../client/modules/email/components/SummaryEmail/MeetingSummaryEmail/MeetingSummaryEmail.tsx" - ] + "lib": ["esnext"] + } } diff --git a/packages/client/components/AnimatedFade.tsx b/packages/client/components/AnimatedFade.tsx deleted file mode 100644 index f1f612ec4ff..00000000000 --- a/packages/client/components/AnimatedFade.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* Deprecated. See internals of useMenuPortal */ - -import {ClassNames} from '@emotion/core' -import React, {Component, ReactNode} from 'react' -import {CSSTransition} from 'react-transition-group' - -interface Props extends CSSTransition { - appear?: boolean - in?: boolean - onExited?: () => void - exit?: boolean - unmountOnExit?: boolean - children: ReactNode - duration?: number - slide?: number -} - -// eslint-disable-next-line -class AnimatedFade extends Component { - render() { - const {children, duration = 100, slide = 32, ...props} = this.props - - const classNames = (css) => { - const enter = css({ - opacity: 0, - transform: `translate3d(0, ${slide}px, 0)` - }) - const enterActive = css({ - opacity: '1 !important' as any, - transform: 'translate3d(0, 0, 0) !important', - transition: `all ${duration}ms ease-in !important` - }) - - const exit = css({ - opacity: 1, - transform: 'translate3d(0, 0, 0)' - }) - - const exitActive = css({ - opacity: '0 !important' as any, - transform: `translate3d(0, ${-slide}px, 0) !important`, - transition: `all ${duration}ms ease-in !important` - }) - return { - appear: enter, - appearActive: enterActive, - enter, - enterActive, - exit, - exitActive - } - } - return ( - - {({css}) => { - return ( - - {children} - - ) - }} - - ) - } -} - -export default AnimatedFade diff --git a/packages/client/components/AzureDevOpsScopingSelectAllIssues.tsx b/packages/client/components/AzureDevOpsScopingSelectAllIssues.tsx index d4f80d54fe0..24c42792294 100644 --- a/packages/client/components/AzureDevOpsScopingSelectAllIssues.tsx +++ b/packages/client/components/AzureDevOpsScopingSelectAllIssues.tsx @@ -3,6 +3,7 @@ import graphql from 'babel-plugin-relay/macro' import React from 'react' import {useFragment} from 'react-relay' import useUnusedRecords from '~/hooks/useUnusedRecords' +import {AzureDevOpsScopingSelectAllIssues_workItems$key} from '../__generated__/AzureDevOpsScopingSelectAllIssues_workItems.graphql' import useAtmosphere from '../hooks/useAtmosphere' import useMutationProps from '../hooks/useMutationProps' import UpdatePokerScopeMutation from '../mutations/UpdatePokerScopeMutation' @@ -11,7 +12,6 @@ import {PALETTE} from '../styles/paletteV3' import {Threshold} from '../types/constEnums' import AzureDevOpsClientManager from '../utils/AzureDevOpsClientManager' import getSelectAllTitle from '../utils/getSelectAllTitle' -import {AzureDevOpsScopingSelectAllIssues_workItems$key} from '../__generated__/AzureDevOpsScopingSelectAllIssues_workItems.graphql' import Checkbox from './Checkbox' const Item = styled('div')({ @@ -42,7 +42,7 @@ interface Props { } const AzureDevOpsScopingSelectAllIssues = (props: Props) => { - const {meetingId, usedServiceTaskIds, workItems: workItemsRef, providerId} = props + const {meetingId, usedServiceTaskIds, workItems: workItemsRef} = props const workItems = useFragment( graphql` fragment AzureDevOpsScopingSelectAllIssues_workItems on AzureDevOpsWorkItemEdge @@ -57,7 +57,7 @@ const AzureDevOpsScopingSelectAllIssues = (props: Props) => { workItemsRef ) const atmosphere = useAtmosphere() - const {onCompleted, onError, submitMutation, submitting, error} = useMutationProps() + const {onCompleted, onError, submitMutation, error} = useMutationProps() const getProjectId = (url: URL) => { const firstIndex = url.pathname.indexOf('/', 1) const seconedIndex = url.pathname.indexOf('/', firstIndex + 1) diff --git a/packages/client/hooks/useAtlassianSites.ts b/packages/client/hooks/useAtlassianSites.ts deleted file mode 100644 index 8fdb3a66567..00000000000 --- a/packages/client/hooks/useAtlassianSites.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {useEffect, useRef, useState} from 'react' -import {Unpromise} from '../types/generics' -import AtlassianClientManager from '../utils/AtlassianClientManager' -import {AccessibleResource} from '../utils/AtlassianManager' - -const useAtlassianSites = (accessToken?: string) => { - const isMountedRef = useRef(true) - const [sites, setSites] = useState([]) - const [status, setStatus] = useState(null) - useEffect(() => { - const manager = new AtlassianClientManager(accessToken || '') - const fetchSites = async () => { - let res: Unpromise> - try { - res = await manager.getAccessibleResources() - } catch (e) { - if (isMountedRef.current) { - setStatus('error') - } - return - } - if (isMountedRef.current) { - if (Array.isArray(res)) { - setStatus('loaded') - setSites(res) - } else { - setStatus('error') - } - } - } - - if (accessToken && isMountedRef.current) { - setStatus('loading') - fetchSites().catch() - } - return () => { - isMountedRef.current = false - } - }, [accessToken]) - return {sites, status} -} - -export default useAtlassianSites diff --git a/packages/client/modules/email/components/MeetingSummaryEmailRootSSR.tsx b/packages/client/modules/email/components/MeetingSummaryEmailRootSSR.tsx index 9ad3310cd7d..bd17389a344 100644 --- a/packages/client/modules/email/components/MeetingSummaryEmailRootSSR.tsx +++ b/packages/client/modules/email/components/MeetingSummaryEmailRootSSR.tsx @@ -2,7 +2,6 @@ import graphql from 'babel-plugin-relay/macro' import {MeetingSummaryEmailRootSSRQuery} from 'parabol-client/__generated__/MeetingSummaryEmailRootSSRQuery.graphql' import React from 'react' import {useLazyLoadQuery} from 'react-relay' -import {Environment} from 'relay-runtime' import {EMAIL_CORS_OPTIONS} from '../../../types/cors' import makeAppURL from '../../../utils/makeAppURL' import MeetingSummaryEmail from './SummaryEmail/MeetingSummaryEmail/MeetingSummaryEmail' diff --git a/packages/client/modules/email/components/NotificationSummaryEmailRoot.tsx b/packages/client/modules/email/components/NotificationSummaryEmailRoot.tsx index bcf16c49bb0..cd610f6758d 100644 --- a/packages/client/modules/email/components/NotificationSummaryEmailRoot.tsx +++ b/packages/client/modules/email/components/NotificationSummaryEmailRoot.tsx @@ -56,7 +56,7 @@ const NotificationSummaryEmailRoot = (props: NotificationSummaryRootProps) => { (edge) => edge.node.status === 'UNREAD' && new Date(edge.node.createdAt) > new Date(Date.now() - ms('1d')) && - NOTIFICATION_TEMPLATE_TYPE[edge.node.type] // Filter down to the notifications that have been implemented. + NOTIFICATION_TEMPLATE_TYPE[edge.node.type as keyof typeof NOTIFICATION_TEMPLATE_TYPE] // Filter down to the notifications that have been implemented. ) .map((edge) => edge.node) .slice(0, MAX_EMAIL_NOTIFICATIONS) diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/MeetingSummaryEmail.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/MeetingSummaryEmail.tsx index 5a2611456f8..b49644af0f5 100644 --- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/MeetingSummaryEmail.tsx +++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/MeetingSummaryEmail.tsx @@ -36,20 +36,6 @@ const pagePadding = { paddingTop: 24 } -declare module 'react' { - interface TdHTMLAttributes { - height?: string | number - width?: string | number - bgcolor?: string - } - interface TableHTMLAttributes { - align?: 'center' | 'left' | 'right' - bgcolor?: string - height?: string | number - width?: string | number - } -} - const PagePadding = () => { return ( diff --git a/packages/client/mutations/handlers/handleUpdateTeamMembers.ts b/packages/client/mutations/handlers/handleUpdateTeamMembers.ts index f62a323f291..6ccecccea3c 100644 --- a/packages/client/mutations/handlers/handleUpdateTeamMembers.ts +++ b/packages/client/mutations/handlers/handleUpdateTeamMembers.ts @@ -1,8 +1,12 @@ +import {RecordProxy, RecordSourceSelectorProxy} from 'relay-runtime' import fromTeamMemberId from '../../utils/relay/fromTeamMemberId' import safeRemoveNodeFromArray from '../../utils/relay/safeRemoveNodeFromArray' import pluralizeHandler from './pluralizeHandler' -const handleUpdateTeamMember = (updatedTeamMember, store) => { +const handleUpdateTeamMember = ( + updatedTeamMember: RecordProxy<{id: string}>, + store: RecordSourceSelectorProxy +) => { if (!updatedTeamMember) return const {teamId} = fromTeamMemberId(updatedTeamMember.getValue('id')) const isNotRemoved = updatedTeamMember.getValue('isNotRemoved') @@ -11,7 +15,7 @@ const handleUpdateTeamMember = (updatedTeamMember, store) => { const sorts = ['preferredName'] if (isNotRemoved) { sorts.forEach((sortBy) => { - const teamMembers = team.getLinkedRecords('teamMembers', {sortBy}) + const teamMembers = team.getLinkedRecords<[]>('teamMembers', {sortBy}) if (!teamMembers) return teamMembers.sort((a, b) => (a.getValue(sortBy) > b.getValue(sortBy) ? 1 : -1)) team.setLinkedRecords(teamMembers, 'teamMembers', {sortBy}) @@ -19,8 +23,7 @@ const handleUpdateTeamMember = (updatedTeamMember, store) => { } else { const teamMemberId = updatedTeamMember.getValue('id') sorts.forEach((sortBy) => { - const teamMembers = team.getLinkedRecords('teamMembers', {sortBy}) - safeRemoveNodeFromArray(teamMemberId, teamMembers, 'teamMembers', { + safeRemoveNodeFromArray(teamMemberId, team, 'teamMembers', { storageKeyArgs: {sortBy} }) }) diff --git a/packages/client/package.json b/packages/client/package.json index 3f105e730fb..51b9eb2aead 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.23.1", + "version": "7.24.1", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -31,7 +31,7 @@ "@types/draft-js": "^0.10.24", "@types/fbjs": "^3.0.4", "@types/humanize-duration": "3.27.1", - "@types/jest": "^29.5.1", + "@types/jest": "^29.5.12", "@types/json2csv": "^4.4.0", "@types/jwt-decode": "^2.1.0", "@types/linkify-it": "^3.0.2", @@ -50,8 +50,6 @@ "@types/stripe-v2": "^2.0.1", "babel-plugin-relay": "^12.0.0", "debug": "^4.1.1", - "eslint": "^8.2.0", - "eslint-config-prettier": "^8.5.0", "eslint-plugin-emotion": "^10.0.14", "eslint-plugin-react": "^7.16.0", "eslint-plugin-react-hooks": "^1.6.1", @@ -75,6 +73,8 @@ "@mui/icons-material": "^5.8.4", "@mui/material": "^5.9.2", "@mui/x-date-pickers": "^6.3.1", + "@radix-ui/react-alert-dialog": "1.0.5", + "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.4", @@ -82,8 +82,6 @@ "@radix-ui/react-scroll-area": "^1.0.3", "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-slot": "^1.0.2", - "@radix-ui/react-avatar": "^1.0.4", - "@radix-ui/react-alert-dialog": "1.0.5", "@radix-ui/react-tooltip": "^1.0.7", "@sentry/browser": "^5.8.0", "@stripe/react-stripe-js": "^1.16.5", diff --git a/packages/client/serviceWorker/sw.ts b/packages/client/serviceWorker/sw.ts index e35d7f86db7..eb19da02807 100644 --- a/packages/client/serviceWorker/sw.ts +++ b/packages/client/serviceWorker/sw.ts @@ -79,8 +79,10 @@ const onFetch = async (event: FetchEvent) => { // request.mode could be 'no-cors' // By fetching the URL without specifying the mode the response will not be opaque const isParabolHosted = url.startsWith(PUBLIC_PATH) || url.startsWith(self.origin) - const req = isParabolHosted ? request.url : request - const networkRes = await fetch(req) + // if one of our assets is not in the service worker cache, then it's either fetched via network or served from the broswer cache. + // The browser cache most likely has incorrect CORS headers set, so we better always fetch from the network. + const req = isParabolHosted ? fetch(request.url, {cache: 'no-store'}) : fetch(request) + const networkRes = await req const cache = await caches.open(DYNAMIC_CACHE) // cloning here because I'm not sure if we must clone before reading the body cache.put(request.url, networkRes.clone()).catch(console.error) diff --git a/packages/client/serviceWorker/tsconfig.json b/packages/client/serviceWorker/tsconfig.json index ec0c5fb0b28..f393dcadb5f 100644 --- a/packages/client/serviceWorker/tsconfig.json +++ b/packages/client/serviceWorker/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { -// "composite": true, "lib": ["esnext", "webworker"], "target": "esnext", "module": "commonjs", @@ -11,6 +10,5 @@ "*" // resolve all absolute imports as imports relative to the client package ] } - }, -// "references": [{"path": "../"}] + } } diff --git a/packages/client/shared/gqlIds/EmbedderChannelId.ts b/packages/client/shared/gqlIds/EmbedderChannelId.ts new file mode 100644 index 00000000000..f1fb49341ef --- /dev/null +++ b/packages/client/shared/gqlIds/EmbedderChannelId.ts @@ -0,0 +1,9 @@ +export const EmbedderChannelId = { + join: (serverId: string) => `embedder:${serverId}`, + split: (id: string) => { + const [, serverId] = id.split(':') + return serverId + } +} + +export default EmbedderChannelId diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 58280d064ea..32bb2473e60 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -3,15 +3,10 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "~/*": ["*"], - "static/*": ["../../static/*"] + "~/*": ["*"] }, "outDir": "lib", - "lib": ["esnext", "dom"], - "types": ["node"] + "lib": ["esnext", "dom"] }, - "files": [ - "client.tsx", - ], "exclude": ["serviceWorker", "**/node_modules"] } diff --git a/packages/client/types/generics.ts b/packages/client/types/generics.ts index 663bbd7ac52..7a1d76dafb0 100644 --- a/packages/client/types/generics.ts +++ b/packages/client/types/generics.ts @@ -100,6 +100,9 @@ export type WithFieldsAsType = { : TObj[K] } +export type Tuple = R['length'] extends N ? R : Tuple +export type ParseInt = T extends `${infer Digit extends number}` ? Digit : never + declare global { interface Array { findLastIndex(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): number diff --git a/packages/client/types/modules.d.ts b/packages/client/types/modules.d.ts index 92293d0cbbd..28b06e27aa7 100644 --- a/packages/client/types/modules.d.ts +++ b/packages/client/types/modules.d.ts @@ -20,6 +20,7 @@ declare module 'emoji-mart/dist-modern/utils/data.js' declare module 'emoji-mart/dist-modern/components/picker/nimble-picker' declare module 'react-textarea-autosize' declare module 'react-copy-to-clipboard' +declare module 'tayden-clusterfck' declare let __webpack_public_path__: string declare const __PRODUCTION__: string diff --git a/packages/client/types/reactHTML4.d.ts b/packages/client/types/reactHTML4.d.ts new file mode 100644 index 00000000000..995d555df4a --- /dev/null +++ b/packages/client/types/reactHTML4.d.ts @@ -0,0 +1,15 @@ +import 'react' + +declare module 'react' { + export interface TdHTMLAttributes { + height?: string | number + width?: string | number + bgcolor?: string + } + export interface TableHTMLAttributes { + align?: 'center' | 'left' | 'right' + bgcolor?: string + height?: string | number + width?: string | number + } +} diff --git a/packages/client/ui/AlertDialog/AlertDialogContent.tsx b/packages/client/ui/AlertDialog/AlertDialogContent.tsx index dbcd0de0582..12d5a79355c 100644 --- a/packages/client/ui/AlertDialog/AlertDialogContent.tsx +++ b/packages/client/ui/AlertDialog/AlertDialogContent.tsx @@ -1,8 +1,8 @@ -import * as React from 'react' import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' import clsx from 'clsx' +import * as React from 'react' import {AlertDialogOverlay} from './AlertDialogOverlay' -import {AlertDialogPortal} from './AlertDialog' +import {AlertDialogPortal} from './AlertDialogPortal' const AlertDialogContent = React.forwardRef< HTMLDivElement, diff --git a/packages/client/ui/AlertDialog/AlertDialogPortal.tsx b/packages/client/ui/AlertDialog/AlertDialogPortal.tsx index c4e8d56916f..f572b2d9baa 100644 --- a/packages/client/ui/AlertDialog/AlertDialogPortal.tsx +++ b/packages/client/ui/AlertDialog/AlertDialogPortal.tsx @@ -1,3 +1,3 @@ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' -const AlertDialogPortal = AlertDialogPrimitive.Portal +export const AlertDialogPortal = AlertDialogPrimitive.Portal diff --git a/packages/client/ui/AlertDialog/AlertDialogTrigger.tsx b/packages/client/ui/AlertDialog/AlertDialogTrigger.tsx index 0a20feddfc1..6b2d6149df4 100644 --- a/packages/client/ui/AlertDialog/AlertDialogTrigger.tsx +++ b/packages/client/ui/AlertDialog/AlertDialogTrigger.tsx @@ -1,3 +1,3 @@ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' -const AlertDialogTrigger = AlertDialogPrimitive.Trigger +export const AlertDialogTrigger = AlertDialogPrimitive.Trigger diff --git a/packages/client/utils/__tests__/parseEmailAddressList.test.ts b/packages/client/utils/__tests__/parseEmailAddressList.test.ts index a9778e7bebe..87c83ee894e 100644 --- a/packages/client/utils/__tests__/parseEmailAddressList.test.ts +++ b/packages/client/utils/__tests__/parseEmailAddressList.test.ts @@ -1,7 +1,12 @@ /* eslint-env jest */ import parseEmailAddressList from '../parseEmailAddressList' -const getAddressStr = (res) => res && res.parsedInvitees.map((val) => val.address).join(', ') +type Res = { + parsedInvitees: { + [other: string]: any + }[] +} +const getAddressStr = (res: Res) => res && res.parsedInvitees.map((val) => val.address).join(', ') describe('parseEmailAddressList', () => { it('validates a simple single email', () => { diff --git a/packages/client/utils/getBezierTimePercentGivenDistancePercent.ts b/packages/client/utils/getBezierTimePercentGivenDistancePercent.ts index af2b6ab346b..3b988aec450 100644 --- a/packages/client/utils/getBezierTimePercentGivenDistancePercent.ts +++ b/packages/client/utils/getBezierTimePercentGivenDistancePercent.ts @@ -10,7 +10,12 @@ const bezierLookup = [] as {x: number; y: number}[] const getBezierTimePercentGivenDistancePercent = (threshold: number, bezierCurve: string) => { if (bezierLookup.length === 0) { const re = /\(([^)]+)\)/ - const [x1, y1, x2, y2] = re.exec(bezierCurve)![1].split(',').map(Number) + const [x1, y1, x2, y2] = re.exec(bezierCurve)![1]!.split(',').map(Number) as [ + number, + number, + number, + number + ] // css bezier-curves imply a start of 0,0 and end of 1,1 const x0 = 0 const y0 = 0 @@ -32,9 +37,9 @@ const getBezierTimePercentGivenDistancePercent = (threshold: number, bezierCurve } let x for (let i = 1; i < bezierLookup.length; i++) { - const point = bezierLookup[i] + const point = bezierLookup[i]! if (point.y > threshold) { - x = bezierLookup[i - 1].x + x = bezierLookup[i - 1]!.x break } } diff --git a/packages/client/utils/screenBugs/Bug.ts b/packages/client/utils/screenBugs/Bug.ts index 536d0142887..0ab7411d40d 100644 --- a/packages/client/utils/screenBugs/Bug.ts +++ b/packages/client/utils/screenBugs/Bug.ts @@ -183,7 +183,7 @@ export default class Bug { this.bug.classList.remove('bug-dead') } - animate = (t) => { + animate = (t: any) => { if (!this.animating || !this.alive || !this.active) return this.going = requestAnimationFrame((t) => { this.animate(t) @@ -217,9 +217,9 @@ export default class Bug { this.angle_deg %= 360 if (this.angle_deg < 0) this.angle_deg += 360 - if (Math.abs(this.directions[this.near_edge] - this.angle_deg) > 15) { - const angle1 = this.directions[this.near_edge] - this.angle_deg - const angle2 = 360 - this.angle_deg + this.directions[this.near_edge] + if (Math.abs(this.directions[this.near_edge]! - this.angle_deg) > 15) { + const angle1 = this.directions[this.near_edge]! - this.angle_deg + const angle2 = 360 - this.angle_deg + this.directions[this.near_edge]! this.large_turn_angle_deg = Math.abs(angle1) < Math.abs(angle2) ? angle1 : angle2 this.edge_test_counter = 10 @@ -399,7 +399,7 @@ export default class Bug { let side = Math.round(Math.random() * 4 - 0.5) const d = document const e = d.documentElement - const g = d.getElementsByTagName('body')[0] + const g = d.getElementsByTagName('body')[0]! const windowX = window.innerWidth || e.clientWidth || g.clientWidth const windowY = window.innerHeight || e.clientHeight || g.clientHeight if (side > 3) side = 3 @@ -455,7 +455,7 @@ export default class Bug { let side = Math.round(Math.random() * 4 - 0.5) const d = document const e = d.documentElement - const g = d.getElementsByTagName('body')[0] + const g = d.getElementsByTagName('body')[0]! const windowX = window.innerWidth || e.clientWidth || g.clientWidth const windowY = window.innerHeight || e.clientHeight || g.clientHeight if (side > 3) side = 3 @@ -496,7 +496,7 @@ export default class Bug { const style = {} as Pos const d = document const e = d.documentElement - const g = d.getElementsByTagName('body')[0] + const g = d.getElementsByTagName('body')[0]! const windowX = window.innerWidth || e.clientWidth || g.clientWidth const windowY = window.innerHeight || e.clientHeight || g.clientHeight @@ -529,11 +529,11 @@ export default class Bug { this.drop(deathType) } - drop = (deathType) => { + drop = (deathType: any) => { const startPos = this.bug.top const d = document const e = d.documentElement - const g = d.getElementsByTagName('body')[0] + const g = d.getElementsByTagName('body')[0]! const pos = window.innerHeight || e.clientHeight || g.clientHeight const finalPos = pos - this.options.bugHeight const rotationRate = this.random(0, 20, true) @@ -545,7 +545,7 @@ export default class Bug { }) } - dropping = (t, startPos, finalPos, rotationRate, deathType) => { + dropping = (t: number, startPos: any, finalPos: any, rotationRate: any, deathType: any) => { const elapsedTime = t - this._lastTimestamp! const deltaPos = 0.002 * (elapsedTime * elapsedTime) let newPos = startPos + deltaPos diff --git a/packages/client/utils/screenBugs/BugController.ts b/packages/client/utils/screenBugs/BugController.ts index 132c1796103..330f729ad61 100644 --- a/packages/client/utils/screenBugs/BugController.ts +++ b/packages/client/utils/screenBugs/BugController.ts @@ -131,7 +131,7 @@ class BugDispatch { this.spawnDelay = [] for (let i = 0; i < numBugs; i++) { const delay = this.random(this.options.minDelay, this.options.maxDelay, true) - const thebug = this.bugs[i] + const thebug = this.bugs[i]! // fly the bug onto the page: this.spawnDelay[i] = window.setTimeout(() => { this.options.canFly ? thebug.flyIn() : thebug.walkIn() @@ -152,30 +152,30 @@ class BugDispatch { stop = () => { for (let i = 0; i < this.bugs.length; i++) { if (this.spawnDelay[i]) clearTimeout(this.spawnDelay[i]) - this.bugs[i].stop() + this.bugs[i]!.stop() } } end = () => { for (let i = 0; i < this.bugs.length; i++) { if (this.spawnDelay[i]) clearTimeout(this.spawnDelay[i]) - this.bugs[i].stop() - this.bugs[i].remove() + this.bugs[i]!.stop() + this.bugs[i]!.remove() } } reset = () => { this.stop() for (let i = 0; i < this.bugs.length; i++) { - this.bugs[i].reset() - this.bugs[i].walkIn() + this.bugs[i]!.reset() + this.bugs[i]!.walkIn() } } killAll = () => { for (let i = 0; i < this.bugs.length; i++) { if (this.spawnDelay[i]) clearTimeout(this.spawnDelay[i]) - this.bugs[i].die() + this.bugs[i]!.die() } } @@ -209,13 +209,13 @@ class BugDispatch { } const numBugs = this.bugs.length for (let i = 0; i < numBugs; i++) { - const pos = this.bugs[i].getPos() + const pos = this.bugs[i]!.getPos() if (pos) { if ( Math.abs(pos.top - posy) + Math.abs(pos.left - posx) < this.options.eventDistanceToBug && - !this.bugs[i].flyperiodical + !this.bugs[i]!.flyperiodical ) { - this.near_bug(this.bugs[i]) + this.near_bug(this.bugs[i]!) } } } @@ -233,7 +233,7 @@ class BugDispatch { let mode = this.options.mouseOver if (mode === 'random') { - mode = this.modes[this.random(0, this.modes.length - 1, true)] + mode = this.modes[this.random(0, this.modes.length - 1, true)]! } if (mode === 'fly') { diff --git a/packages/embedder/EMBEDDER_JOB_PRIORITY.ts b/packages/embedder/EMBEDDER_JOB_PRIORITY.ts new file mode 100644 index 00000000000..a54e4b5c67d --- /dev/null +++ b/packages/embedder/EMBEDDER_JOB_PRIORITY.ts @@ -0,0 +1,6 @@ +export const EMBEDDER_JOB_PRIORITY = { + MEETING: 40, + DEFAULT: 50, + TOPIC_HISTORY: 80, + NEW_MODEL: 90 +} as const diff --git a/packages/embedder/EmbeddingsJobQueueStream.ts b/packages/embedder/EmbeddingsJobQueueStream.ts new file mode 100644 index 00000000000..64f7de936dd --- /dev/null +++ b/packages/embedder/EmbeddingsJobQueueStream.ts @@ -0,0 +1,73 @@ +import {Selectable, sql} from 'kysely' +import ms from 'ms' +import sleep from 'parabol-client/utils/sleep' +import 'parabol-server/initSentry' +import getKysely from 'parabol-server/postgres/getKysely' +import {DB} from 'parabol-server/postgres/pg' +import RootDataLoader from '../server/dataloader/RootDataLoader' +import {processJob} from './processJob' +import {Logger} from '../server/utils/Logger' +import {EmbeddingsTableName} from './ai_models/AbstractEmbeddingsModel' + +export type DBJob = Selectable +export type EmbedJob = DBJob & { + jobType: 'embed' + jobData: { + embeddingsMetadataId: number + model: EmbeddingsTableName + } +} +export type RerankJob = DBJob & {jobType: 'rerank'; jobData: {discussionIds: string[]}} +export type Job = EmbedJob | RerankJob + +export class EmbeddingsJobQueueStream implements AsyncIterableIterator { + [Symbol.asyncIterator]() { + return this + } + dataLoader = new RootDataLoader({maxBatchSize: 1000}) + async next(): Promise> { + const pg = getKysely() + const getJob = (isFailed: boolean) => { + return pg + .with( + (cte) => cte('ids').materialized(), + (db) => + db + .selectFrom('EmbeddingsJobQueue') + .select('id') + .orderBy(['priority']) + .$if(!isFailed, (db) => db.where('state', '=', 'queued')) + .$if(isFailed, (db) => + db.where('state', '=', 'failed').where('retryAfter', '<', new Date()) + ) + .limit(1) + .forUpdate() + .skipLocked() + ) + .updateTable('EmbeddingsJobQueue') + .set({state: 'running', startAt: new Date()}) + .where('id', '=', sql`ANY(SELECT id FROM ids)`) + .returningAll() + .executeTakeFirst() + } + const job = (await getJob(false)) || (await getJob(true)) + if (!job) { + Logger.log('JobQueueStream: no jobs found') + // queue is empty, so sleep for a while + await sleep(ms('1m')) + return this.next() + } + + const isSuccessful = await processJob(job as Job, this.dataLoader) + if (isSuccessful) { + await pg.deleteFrom('EmbeddingsJobQueue').where('id', '=', job.id).executeTakeFirstOrThrow() + } + return {done: false, value: job as Job} + } + return() { + return Promise.resolve({done: true as const, value: undefined}) + } + throw(error: any) { + return Promise.resolve({done: true, value: error}) + } +} diff --git a/packages/embedder/README.md b/packages/embedder/README.md index fc3fc68f335..36bb8e2ea50 100644 --- a/packages/embedder/README.md +++ b/packages/embedder/README.md @@ -3,27 +3,14 @@ This service builds embedding vectors for semantic search and for other AI/ML use cases. It does so by: -1. Updating a list of all possible items to create embedding vectors for and - storing that list in the `EmbeddingsMetadata` table -2. Adding these items in batches to the `EmbeddingsJobQueue` table and a redis - priority queue called `embedder:queue` -3. Allowing one or more parallel embedding services to calculate embedding - vectors (EmbeddingJobQueue states transistion from `queued` -> `embedding`, - then `embedding` -> [deleting the `EmbeddingJobQueue` row] - - In addition to deleteing the `EmbeddingJobQueue` row, when a job completes - successfully: - - - A row is added to the model table with the embedding vector; the - `EmbeddingMetadataId` field on this row points the appropriate - metadata row on `EmbeddingsMetadata` - - The `EmbeddingsMetadata.models` array is updated with the name of the - table that the embedding has been generated for - -4. This process repeats forever using a silly polling loop - -In the future, it would be wonderful to enhance this service such that it were -event driven. +1. Homogenizes different types of data into a single `EmbeddingsMetadata` table +2. Each new row in `EmbeddingsMetadata` creates a new row in `EmbeddingsJobQueue` for each model +3. Uses PG to pick a job from the queue and sets the job from `queued` -> `embedding`, + then `embedding` -> [deleting the `EmbeddingJobQueue` row] +4. Embedding involves creating a `fullText` from the work item and then a vector from that `fullText` +5. New jobs to add metadata are sent via redis streams from the GQL Executor +6. If embedding fails, the application increments the `retryCount` and increases the `retryAfter` if a retry is desired +7. If a job gets stalled, a process that runs every 5 minutes will look for jobs older than 5 minutes and reset them to `queued` ## Prerequisites @@ -37,10 +24,9 @@ The predeploy script checks for an environment variable The Embedder service takes no arguments and is controlled by the following environment variables, here given with example configuration: -- `AI_EMBEDDER_ENABLE`: enable/disable the embedder service from - performing work, or sleeping indefinitely +- `AI_EMBEDDER_WORKERS`: How many workers should simultaneously pick jobs from the queue. If less than 1, disabled. -`AI_EMBEDDER_ENABLED='true'` +`AI_EMBEDDER_WORKERS='1'` - `AI_EMBEDDING_MODELS`: JSON configuration for which embedding models are enabled. Each model in the array will be instantiated by @@ -69,3 +55,10 @@ environment variables, here given with example configuration: The Embedder service is stateless and takes no arguments. Multiple instances of the service may be started in order to match embedding load, or to catch up on history more quickly. + +## Resources + +### PG as a Job Queue + +- https://leontrolski.github.io/postgres-as-queue.html +- https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/ diff --git a/packages/embedder/addEmbeddingsMetadata.ts b/packages/embedder/addEmbeddingsMetadata.ts new file mode 100644 index 00000000000..214fecc0409 --- /dev/null +++ b/packages/embedder/addEmbeddingsMetadata.ts @@ -0,0 +1,15 @@ +import {addEmbeddingsMetadataForRetrospectiveDiscussionTopic} from './addEmbeddingsMetadataForRetrospectiveDiscussionTopic' +import {MessageToEmbedder} from './custom' + +export const addEmbeddingsMetadata = async ({objectTypes, ...options}: MessageToEmbedder) => { + return Promise.all( + objectTypes.map((type) => { + switch (type) { + case 'retrospectiveDiscussionTopic': + return addEmbeddingsMetadataForRetrospectiveDiscussionTopic(options) + default: + throw new Error(`Invalid object type: ${type}`) + } + }) + ) +} diff --git a/packages/embedder/addEmbeddingsMetadataForRetrospectiveDiscussionTopic.ts b/packages/embedder/addEmbeddingsMetadataForRetrospectiveDiscussionTopic.ts new file mode 100644 index 00000000000..cd4489e3942 --- /dev/null +++ b/packages/embedder/addEmbeddingsMetadataForRetrospectiveDiscussionTopic.ts @@ -0,0 +1,144 @@ +import {ExpressionOrFactory, SqlBool, sql} from 'kysely' +import getRethink from 'parabol-server/database/rethinkDriver' +import {RDatum} from 'parabol-server/database/stricterR' +import getKysely from 'parabol-server/postgres/getKysely' +import {DB} from 'parabol-server/postgres/pg' +import {Logger} from 'parabol-server/utils/Logger' +import {EMBEDDER_JOB_PRIORITY} from './EMBEDDER_JOB_PRIORITY' +import getModelManager from './ai_models/ModelManager' +import {EmbedderOptions} from './custom' + +interface DiscussionMeta { + id: string + teamId: string + createdAt: Date +} + +const validateDiscussions = async (discussions: (DiscussionMeta & {meetingId: string})[]) => { + const r = await getRethink() + if (discussions.length === 0) return discussions + // Exclude discussions that belong to an unfinished meeting + const meetingIds = [...new Set(discussions.map(({meetingId}) => meetingId))] + const endedMeetingIds = await r + .table('NewMeeting') + .getAll(r.args(meetingIds), {index: 'id'}) + .filter((row: RDatum) => row('endedAt').default(null).ne(null))('id') + .distinct() + .run() + const endedMeetingIdsSet = new Set(endedMeetingIds) + return discussions.filter(({meetingId}) => endedMeetingIdsSet.has(meetingId)) +} + +const insertDiscussionsIntoMetadata = async (discussions: DiscussionMeta[], priority: number) => { + const pg = getKysely() + const metadataRows = discussions.map(({id, teamId, createdAt}) => ({ + refId: id, + objectType: 'retrospectiveDiscussionTopic' as const, + teamId, + // Not techincally updatedAt since discussions are be updated after they get created + refUpdatedAt: createdAt + })) + if (!metadataRows[0]) return + + const modelManager = getModelManager() + const tableNames = [...modelManager.embeddingModels.keys()] + return ( + pg + .with('Insert', (qc) => + qc + .insertInto('EmbeddingsMetadata') + .values(metadataRows) + .onConflict((oc) => oc.doNothing()) + .returning('id') + ) + // create n*m rows for n models & m discussions + .with('Metadata', (qc) => + qc + .selectFrom('Insert') + .fullJoin( + sql<{model: string}>`UNNEST(ARRAY[${sql.join(tableNames)}])`.as('model'), + (join) => join.onTrue() + ) + .select(['id', 'model']) + ) + .insertInto('EmbeddingsJobQueue') + .columns(['jobType', 'priority', 'jobData']) + .expression(({selectFrom}) => + selectFrom('Metadata').select(({lit, fn, ref}) => [ + sql.lit('embed').as('jobType'), + lit(priority).as('priority'), + fn('json_build_object', [ + sql.lit('embeddingsMetadataId'), + ref('Metadata.id'), + sql.lit('model'), + ref('Metadata.model') + ]).as('jobData') + ]) + ) + .execute() + ) +} + +export const addEmbeddingsMetadataForRetrospectiveDiscussionTopic = async ({ + startAt, + endAt, + meetingId +}: EmbedderOptions) => { + // load up the metadata table will all discussion topics that are a part of meetings ended within the given date range + const pg = getKysely() + if (meetingId) { + const discussions = await pg + .selectFrom('Discussion') + .select(['id', 'teamId', 'createdAt']) + .where('meetingId', '=', meetingId) + .execute() + await insertDiscussionsIntoMetadata(discussions, EMBEDDER_JOB_PRIORITY.MEETING) + return + } + // PG only accepts 65K parameters (inserted columns * number of rows + query params). Make the batches as big as possible + const PG_MAX_PARAMS = 65535 + const QUERY_PARAMS = 10 + const METADATA_COLS_PER_ROW = 4 + const BATCH_SIZE = Math.floor((PG_MAX_PARAMS - QUERY_PARAMS) / METADATA_COLS_PER_ROW) + const pgStartAt = startAt || new Date(0) + const pgEndAt = (endAt || new Date('4000-01-01')).getTime() / 1000 + + let curEndAt = pgEndAt + let curEndId = '' + for (let i = 0; i < 1e6; i++) { + // preserve microsecond resolution to keep timestamps equal + // so we can use the ID as a tiebreaker when count(createdAt) > BATCH_SIZE + const pgTime = sql`to_timestamp(${curEndAt})` + const lessThanTimeOrId: ExpressionOrFactory = curEndId + ? ({eb}) => + eb('createdAt', '<', pgTime).or(eb('createdAt', '=', pgTime).and('id', '>', curEndId)) + : ({eb}) => eb('createdAt', '<=', pgTime) + const discussions = await pg + .selectFrom('Discussion') + .select([ + 'id', + 'teamId', + 'createdAt', + 'meetingId', + sql`extract(epoch from "createdAt")`.as('createdAtEpoch') + ]) + .where('createdAt', '>', pgStartAt) + .where(lessThanTimeOrId) + .where('discussionTopicType', '=', 'reflectionGroup') + .orderBy('createdAt', 'desc') + .orderBy('id') + .limit(BATCH_SIZE) + .execute() + const earliestDiscussionInBatch = discussions.at(-1) + if (!earliestDiscussionInBatch) break + const {createdAtEpoch, id} = earliestDiscussionInBatch + curEndId = curEndAt === createdAtEpoch ? id : '' + curEndAt = createdAtEpoch + const validDiscussions = await validateDiscussions(discussions) + await insertDiscussionsIntoMetadata(validDiscussions, EMBEDDER_JOB_PRIORITY.TOPIC_HISTORY) + const jsTime = new Date(createdAtEpoch * 1000) + Logger.log( + `Inserted ${validDiscussions.length}/${discussions.length} discussions in metadata ending at ${jsTime}` + ) + } +} diff --git a/packages/embedder/ai_models/AbstractEmbeddingsModel.ts b/packages/embedder/ai_models/AbstractEmbeddingsModel.ts new file mode 100644 index 00000000000..f9fb669737d --- /dev/null +++ b/packages/embedder/ai_models/AbstractEmbeddingsModel.ts @@ -0,0 +1,154 @@ +import {sql} from 'kysely' +import getKysely from 'parabol-server/postgres/getKysely' +import {DB} from 'parabol-server/postgres/pg' +import isValid from '../../server/graphql/isValid' +import {Logger} from '../../server/utils/Logger' +import {EMBEDDER_JOB_PRIORITY} from '../EMBEDDER_JOB_PRIORITY' +import {ISO6391} from '../iso6393To1' +import {AbstractModel} from './AbstractModel' + +export interface EmbeddingModelParams { + embeddingDimensions: number + maxInputTokens: number + tableSuffix: string + languages: ISO6391[] +} +export type EmbeddingsTableName = `Embeddings_${string}` +export type EmbeddingsTable = Extract + +export abstract class AbstractEmbeddingsModel extends AbstractModel { + readonly embeddingDimensions: number + readonly maxInputTokens: number + readonly tableName: EmbeddingsTableName + readonly languages: ISO6391[] + constructor(modelId: string, url: string) { + super(url) + const modelParams = this.constructModelParams(modelId) + this.embeddingDimensions = modelParams.embeddingDimensions + this.languages = modelParams.languages + this.maxInputTokens = modelParams.maxInputTokens + this.tableName = `Embeddings_${modelParams.tableSuffix}` + } + protected abstract constructModelParams(modelId: string): EmbeddingModelParams + abstract getEmbedding(content: string, retries?: number): Promise + + abstract getTokens(content: string): Promise + + async chunkText(content: string) { + const tokens = await this.getTokens(content) + if (tokens instanceof Error) return tokens + const isFullTextTooBig = tokens.length > this.maxInputTokens + if (!isFullTextTooBig) return [content] + + for (let i = 0; i < 3; i++) { + const tokensPerWord = (4 + i) / 3 + const chunks = this.splitText(content, tokensPerWord) + const chunkLengths = await Promise.all( + chunks.map(async (chunk) => { + const chunkTokens = await this.getTokens(chunk) + if (chunkTokens instanceof Error) return chunkTokens + return chunkTokens.length + }) + ) + const firstError = chunkLengths.find( + (chunkLength): chunkLength is Error => chunkLength instanceof Error + ) + if (firstError) return firstError + + const validChunks = chunkLengths.filter(isValid) + if (validChunks.every((chunkLength) => chunkLength <= this.maxInputTokens)) { + return chunks + } + } + return new Error(`Text is too long and could not be split into chunks. Is it english?`) + } + // private because result must still be too long to go into model. Must verify with getTokens + private splitText(content: string, tokensPerWord = 4 / 3) { + // it's actually 4 / 3, but don't want to chance a failed split + const WORD_LIMIT = Math.floor(this.maxInputTokens / tokensPerWord) + const chunks: string[] = [] + const delimiters = ['\n\n', '\n', '.', ' '] + const countWords = (text: string) => text.trim().split(/\s+/).length + const splitOnDelimiter = (text: string, delimiter: string) => { + const sections = text.split(delimiter) + for (let i = 0; i < sections.length; i++) { + const section = sections[i]! + const sectionWordCount = countWords(section) + if (sectionWordCount < WORD_LIMIT) { + // try to merge this section with the last one + const previousSection = chunks.at(-1) + if (previousSection) { + const combinedChunks = `${previousSection}${delimiter}${section}` + const mergedWordCount = countWords(combinedChunks) + if (mergedWordCount < WORD_LIMIT) { + chunks[chunks.length - 1] = combinedChunks + continue + } + } + chunks.push(section) + } else { + const nextDelimiter = delimiters[delimiters.indexOf(delimiter) + 1]! + splitOnDelimiter(section, nextDelimiter) + } + } + } + splitOnDelimiter(content.trim(), delimiters[0]!) + return chunks + } + + async createEmbeddingsForModel() { + Logger.log(`Queueing EmbeddingsMetadata into EmbeddingsJobQueue for ${this.tableName}`) + const pg = getKysely() + await pg + .insertInto('EmbeddingsJobQueue') + .columns(['jobData', 'priority']) + .expression(({selectFrom}) => + selectFrom('EmbeddingsMetadata') + .select(({fn, lit}) => [ + fn('json_build_object', [ + sql.lit('model'), + sql.lit(this.tableName), + sql.lit('embeddingsMetadataId'), + 'id' + ]).as('jobData'), + lit(EMBEDDER_JOB_PRIORITY.NEW_MODEL).as('priority') + ]) + .where('language', 'in', this.languages) + ) + .onConflict((oc) => oc.doNothing()) + .execute() + } + async createTable() { + const pg = getKysely() + const hasTable = + ( + await sql`SELECT 1 FROM ${sql.id('pg_catalog', 'pg_tables')} WHERE ${sql.id( + 'tablename' + )} = ${this.tableName}`.execute(pg) + ).rows.length > 0 + if (hasTable) return + const vectorDimensions = this.embeddingDimensions + Logger.log(`ModelManager: creating ${this.tableName} with ${vectorDimensions} dimensions`) + await sql` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS ${sql.id(this.tableName)} ( + "id" INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "embedText" TEXT, + "embedding" vector(${sql.raw(vectorDimensions.toString())}), + "embeddingsMetadataId" INTEGER UNIQUE NOT NULL, + "chunkNumber" SMALLINT, + UNIQUE("embeddingsMetadataId", "chunkNumber"), + FOREIGN KEY ("embeddingsMetadataId") + REFERENCES "EmbeddingsMetadata"("id") + ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS "idx_${sql.raw(this.tableName)}_embedding_vector_cosign_ops" + ON ${sql.id(this.tableName)} + USING hnsw ("embedding" vector_cosine_ops); + END + $$; + `.execute(pg) + await this.createEmbeddingsForModel() + } +} diff --git a/packages/embedder/ai_models/AbstractGenerationModel.ts b/packages/embedder/ai_models/AbstractGenerationModel.ts new file mode 100644 index 00000000000..ac0223e6eb6 --- /dev/null +++ b/packages/embedder/ai_models/AbstractGenerationModel.ts @@ -0,0 +1,25 @@ +import {AbstractModel} from './AbstractModel' + +export interface GenerationOptions { + maxNewTokens?: number + seed?: number + stop?: string + temperature?: number + topK?: number + topP?: number +} +export interface GenerationModelParams { + maxInputTokens: number +} + +export abstract class AbstractGenerationModel extends AbstractModel { + readonly maxInputTokens: number + constructor(modelId: string, url: string) { + super(url) + const modelParams = this.constructModelParams(modelId) + this.maxInputTokens = modelParams.maxInputTokens + } + + protected abstract constructModelParams(modelId: string): GenerationModelParams + abstract summarize(content: string, options: GenerationOptions): Promise +} diff --git a/packages/embedder/ai_models/AbstractModel.ts b/packages/embedder/ai_models/AbstractModel.ts index b57d220cd35..322476c552f 100644 --- a/packages/embedder/ai_models/AbstractModel.ts +++ b/packages/embedder/ai_models/AbstractModel.ts @@ -1,73 +1,15 @@ -export interface ModelConfig { - model: string - url: string -} - -export interface EmbeddingModelConfig extends ModelConfig { - tableSuffix: string -} - -export interface GenerationModelConfig extends ModelConfig {} - export abstract class AbstractModel { - public readonly url?: string + public readonly url: string - constructor(config: ModelConfig) { - this.url = this.normalizeUrl(config.url) + constructor(url: string) { + this.url = this.normalizeUrl(url) } // removes a trailing slash from the inputUrl - private normalizeUrl(inputUrl: string | undefined) { - if (!inputUrl) return undefined + private normalizeUrl(inputUrl: string) { const regex = /[/]+$/ return inputUrl.replace(regex, '') } } -export interface EmbeddingModelParams { - embeddingDimensions: number - maxInputTokens: number - tableSuffix: string -} - -export abstract class AbstractEmbeddingsModel extends AbstractModel { - readonly embeddingDimensions: number - readonly maxInputTokens: number - readonly tableName: string - constructor(config: EmbeddingModelConfig) { - super(config) - const modelParams = this.constructModelParams(config) - this.embeddingDimensions = modelParams.embeddingDimensions - this.maxInputTokens = modelParams.maxInputTokens - this.tableName = `Embeddings_${modelParams.tableSuffix}` - } - protected abstract constructModelParams(config: EmbeddingModelConfig): EmbeddingModelParams - abstract getEmbedding(content: string): Promise -} - -export interface GenerationModelParams { - maxInputTokens: number -} - -export interface GenerationOptions { - maxNewTokens?: number - seed?: number - stop?: string - temperature?: number - topK?: number - topP?: number -} - -export abstract class AbstractGenerationModel extends AbstractModel { - readonly maxInputTokens: number - constructor(config: GenerationModelConfig) { - super(config) - const modelParams = this.constructModelParams(config) - this.maxInputTokens = modelParams.maxInputTokens - } - - protected abstract constructModelParams(config: GenerationModelConfig): GenerationModelParams - abstract summarize(content: string, options: GenerationOptions): Promise -} - export default AbstractModel diff --git a/packages/embedder/ai_models/ModelManager.ts b/packages/embedder/ai_models/ModelManager.ts index bf6888378c8..bbbfa4f6273 100644 --- a/packages/embedder/ai_models/ModelManager.ts +++ b/packages/embedder/ai_models/ModelManager.ts @@ -1,155 +1,93 @@ -import {Kysely, sql} from 'kysely' - -import { - AbstractEmbeddingsModel, - AbstractGenerationModel, - EmbeddingModelConfig, - GenerationModelConfig, - ModelConfig -} from './AbstractModel' +import {AbstractEmbeddingsModel, EmbeddingsTableName} from './AbstractEmbeddingsModel' +import {AbstractGenerationModel} from './AbstractGenerationModel' import OpenAIGeneration from './OpenAIGeneration' import TextEmbeddingsInference from './TextEmbeddingsInference' import TextGenerationInference from './TextGenerationInference' -interface ModelManagerConfig { - embeddingModels: EmbeddingModelConfig[] - generationModels: GenerationModelConfig[] -} +type EmbeddingsModelType = 'text-embeddings-inference' +type GenerationModelType = 'openai' | 'text-generation-inference' -export type EmbeddingsModelType = 'text-embeddings-inference' -export type GenerationModelType = 'openai' | 'text-generation-inference' +export interface ModelConfig { + model: `${EmbeddingsModelType | GenerationModelType}:${string}` + url: string +} export class ModelManager { - embeddingModels: AbstractEmbeddingsModel[] - embeddingModelsMapByTable: {[key: string]: AbstractEmbeddingsModel} - generationModels: AbstractGenerationModel[] - - private isValidConfig( - maybeConfig: Partial - ): maybeConfig is ModelManagerConfig { - if (!maybeConfig.embeddingModels || !Array.isArray(maybeConfig.embeddingModels)) { - throw new Error('Invalid configuration: embedding_models is missing or not an array') + embeddingModels: Map + generationModels: Map + + private parseModelEnvVars(envVar: 'AI_EMBEDDING_MODELS' | 'AI_GENERATION_MODELS'): ModelConfig[] { + const envValue = process.env[envVar] + if (!envValue) return [] + let models + try { + models = JSON.parse(envValue) + } catch (e) { + throw new Error(`Invalid Env Var: ${envVar}. Must be a valid JSON`) } - if (!maybeConfig.generationModels || !Array.isArray(maybeConfig.generationModels)) { - throw new Error('Invalid configuration: summarization_models is missing or not an array') - } - - maybeConfig.embeddingModels.forEach((model: ModelConfig) => { - this.isValidModelConfig(model) - }) - maybeConfig.generationModels.forEach((model: ModelConfig) => { - this.isValidModelConfig(model) - }) - - return true - } - - private isValidModelConfig(model: ModelConfig): model is ModelConfig { - if (typeof model.model !== 'string') { - throw new Error('Invalid ModelConfig: model field should be a string') + if (!Array.isArray(models)) { + throw new Error(`Invalid Env Var: ${envVar}. Must be an array`) } - if (model.url !== undefined && typeof model.url !== 'string') { - throw new Error('Invalid ModelConfig: url field should be a string') - } - - return true - } - - constructor(config: ModelManagerConfig) { - // Validate configuration - this.isValidConfig(config) - - // Initialize embeddings models - this.embeddingModelsMapByTable = {} - this.embeddingModels = config.embeddingModels.map((modelConfig) => { - const [modelType] = modelConfig.model.split(':') as [EmbeddingsModelType, string] - - switch (modelType) { - case 'text-embeddings-inference': { - const embeddingsModel = new TextEmbeddingsInference(modelConfig) - this.embeddingModelsMapByTable[embeddingsModel.tableName] = embeddingsModel - return embeddingsModel + const properties = ['model', 'url'] + models.forEach((model, idx) => { + properties.forEach((prop) => { + if (typeof model[prop] !== 'string') { + throw new Error(`Invalid Env Var: ${envVar}. Invalid "${prop}" at index ${idx}`) } - default: - throw new Error(`unsupported embeddings model '${modelType}'`) - } + }) }) + return models + } - // Initialize summarization models - this.generationModels = config.generationModels.map((modelConfig) => { - const [modelType, _] = modelConfig.model.split(':') as [GenerationModelType, string] - - switch (modelType) { - case 'openai': { - return new OpenAIGeneration(modelConfig) + constructor() { + // Initialize embeddings models + const embeddingConfig = this.parseModelEnvVars('AI_EMBEDDING_MODELS') + this.embeddingModels = new Map( + embeddingConfig.map((modelConfig) => { + const {model, url} = modelConfig + const [modelType, modelId] = model.split(':') as [EmbeddingsModelType, string] + switch (modelType) { + case 'text-embeddings-inference': { + const embeddingsModel = new TextEmbeddingsInference(modelId, url) + return [embeddingsModel.tableName, embeddingsModel] + } + default: + throw new Error(`unsupported embeddings model '${modelType}'`) } - case 'text-generation-inference': { - return new TextGenerationInference(modelConfig) + }) + ) + + // Initialize generation models + const generationConfig = this.parseModelEnvVars('AI_GENERATION_MODELS') + this.generationModels = new Map( + generationConfig.map((modelConfig) => { + const {model, url} = modelConfig + const [modelType, modelId] = model.split(':') as [GenerationModelType, string] + switch (modelType) { + case 'openai': { + return [modelId, new OpenAIGeneration(modelId, url)] + } + case 'text-generation-inference': { + return [modelId, new TextGenerationInference(modelId, url)] + } + default: + throw new Error(`unsupported generation model '${modelType}'`) } - default: - throw new Error(`unsupported summarization model '${modelType}'`) - } - }) + }) + ) } - async maybeCreateTables(pg: Kysely) { - const maybePromises = this.embeddingModels.map(async (embeddingsModel) => { - const tableName = embeddingsModel.tableName - const hasTable = - ( - await sql`SELECT 1 FROM ${sql.id('pg_catalog', 'pg_tables')} WHERE ${sql.id( - 'tablename' - )} = ${tableName}`.execute(pg) - ).rows.length > 0 - if (hasTable) return undefined - const vectorDimensions = embeddingsModel.embeddingDimensions - console.log(`ModelManager: creating ${tableName} with ${vectorDimensions} dimensions`) - const query = sql` - DO $$ - BEGIN - CREATE TABLE IF NOT EXISTS ${sql.id(tableName)} ( - "id" INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "embedText" TEXT, - "embedding" vector(${sql.raw(vectorDimensions.toString())}), - "embeddingsMetadataId" INTEGER NOT NULL, - FOREIGN KEY ("embeddingsMetadataId") - REFERENCES "EmbeddingsMetadata"("id") - ON DELETE CASCADE - ); - CREATE INDEX IF NOT EXISTS "idx_${sql.raw(tableName)}_embedding_vector_cosign_ops" - ON ${sql.id(tableName)} - USING hnsw ("embedding" vector_cosine_ops); - END $$; - - ` - return query.execute(pg) - }) - Promise.all(maybePromises) + async maybeCreateTables() { + return Promise.all([...this.embeddingModels].map(([, model]) => model.createTable())) } } let modelManager: ModelManager | undefined export function getModelManager() { - if (modelManager) return modelManager - const {AI_EMBEDDING_MODELS, AI_GENERATION_MODELS} = process.env - const config: ModelManagerConfig = { - embeddingModels: [], - generationModels: [] - } - try { - config.embeddingModels = AI_EMBEDDING_MODELS && JSON.parse(AI_EMBEDDING_MODELS) - } catch (e) { - throw new Error(`Invalid AI_EMBEDDING_MODELS .env JSON: ${e}`) + if (!modelManager) { + modelManager = new ModelManager() } - try { - config.generationModels = AI_GENERATION_MODELS && JSON.parse(AI_GENERATION_MODELS) - } catch (e) { - throw new Error(`Invalid AI_GENERATION_MODELS .env JSON: ${e}`) - } - - modelManager = new ModelManager(config) - return modelManager } diff --git a/packages/embedder/ai_models/OpenAIGeneration.ts b/packages/embedder/ai_models/OpenAIGeneration.ts index b5614b608c5..2bd97a32822 100644 --- a/packages/embedder/ai_models/OpenAIGeneration.ts +++ b/packages/embedder/ai_models/OpenAIGeneration.ts @@ -1,12 +1,9 @@ import OpenAI from 'openai' import { AbstractGenerationModel, - GenerationModelConfig, GenerationModelParams, GenerationOptions -} from './AbstractModel' - -const MAX_REQUEST_TIME_S = 3 * 60 +} from './AbstractGenerationModel' export type ModelId = 'gpt-3.5-turbo-0125' | 'gpt-4-turbo-preview' @@ -21,16 +18,12 @@ const modelIdDefinitions: Record = { } } -function isValidModelId(object: any): object is ModelId { - return Object.keys(modelIdDefinitions).includes(object) -} - export class OpenAIGeneration extends AbstractGenerationModel { private openAIApi: OpenAI | null - private modelId: ModelId + private modelId!: ModelId - constructor(config: GenerationModelConfig) { - super(config) + constructor(modelId: string, url: string) { + super(modelId, url) if (!process.env.OPEN_AI_API_KEY) { this.openAIApi = null return @@ -75,19 +68,10 @@ export class OpenAIGeneration extends AbstractGenerationModel { throw e } } - protected constructModelParams(config: GenerationModelConfig): GenerationModelParams { - const modelConfigStringSplit = config.model.split(':') - if (modelConfigStringSplit.length != 2) { - throw new Error('OpenAIGeneration model string must be colon-delimited and len 2') - } - - const maybeModelId = modelConfigStringSplit[1] - if (!isValidModelId(maybeModelId)) - throw new Error(`OpenAIGeneration model id unknown: ${maybeModelId}`) - - this.modelId = maybeModelId - - return modelIdDefinitions[maybeModelId] + protected constructModelParams(modelId: string): GenerationModelParams { + const modelParams = modelIdDefinitions[modelId as keyof typeof modelIdDefinitions] + if (!modelParams) throw new Error(`Unknown modelId ${modelId} for OpenAIGeneration`) + return modelParams } } diff --git a/packages/embedder/ai_models/TextEmbeddingsInference.ts b/packages/embedder/ai_models/TextEmbeddingsInference.ts index 549fadcd6fd..1a2a02596b3 100644 --- a/packages/embedder/ai_models/TextEmbeddingsInference.ts +++ b/packages/embedder/ai_models/TextEmbeddingsInference.ts @@ -1,70 +1,86 @@ -import {AbstractEmbeddingsModel, EmbeddingModelConfig, EmbeddingModelParams} from './AbstractModel' -import fetchWithRetry from './helpers/fetchWithRetry' - -const MAX_REQUEST_TIME_S = 3 * 60 - +import createClient from 'openapi-fetch' +import sleep from 'parabol-client/utils/sleep' +import type {paths} from '../textEmbeddingsnterface' +import {AbstractEmbeddingsModel, EmbeddingModelParams} from './AbstractEmbeddingsModel' export type ModelId = 'BAAI/bge-large-en-v1.5' | 'llmrails/ember-v1' const modelIdDefinitions: Record = { 'BAAI/bge-large-en-v1.5': { embeddingDimensions: 1024, maxInputTokens: 512, - tableSuffix: 'bge_l_en_1p5' + tableSuffix: 'bge_l_en_1p5', + languages: ['en'] }, 'llmrails/ember-v1': { embeddingDimensions: 1024, maxInputTokens: 512, - tableSuffix: 'ember_1' + tableSuffix: 'ember_1', + languages: ['en'] } } -function isValidModelId(object: any): object is ModelId { - return Object.keys(modelIdDefinitions).includes(object) -} - export class TextEmbeddingsInference extends AbstractEmbeddingsModel { - constructor(config: EmbeddingModelConfig) { - super(config) + client: ReturnType> + constructor(modelId: string, url: string) { + super(modelId, url) + this.client = createClient({baseUrl: this.url}) } - public async getEmbedding(content: string) { - const fetchOptions = { - body: JSON.stringify({inputs: content}), - deadline: new Date(new Date().getTime() + MAX_REQUEST_TIME_S * 1000), - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json; charset=utf-8' - }, - method: 'POST' + async getTokens(content: string) { + try { + const {data, error} = await this.client.POST('/tokenize', { + body: {inputs: content, add_special_tokens: true}, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json; charset=utf-8' + } + }) + if (error) return new Error(error.error) + return data[0]!.map(({id}) => id) + } catch (e) { + return e instanceof Error ? e : new Error(e as string) } + } + async decodeTokens(inputIds: number[]) { try { - const res = await fetchWithRetry(`${this.url}/embed`, fetchOptions) - const listOfVectors = (await res.json()) as Array - if (!listOfVectors) - throw new Error('TextEmbeddingsInference.getEmbeddings(): listOfVectors is undefined') - if (listOfVectors.length !== 1 || !listOfVectors[0]) - throw new Error( - `TextEmbeddingsInference.getEmbeddings(): listOfVectors list length !== 1 (length: ${listOfVectors.length})` - ) - return listOfVectors[0] + const {data, error} = await this.client.POST('/decode', { + body: {ids: inputIds}, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json; charset=utf-8' + } + }) + if (error) return new Error(error.error) + return data } catch (e) { - console.log(`TextEmbeddingsInference.getEmbeddings() timeout: `, e) - throw e + return e instanceof Error ? e : new Error(e as string) } } - - protected constructModelParams(config: EmbeddingModelConfig): EmbeddingModelParams { - const modelConfigStringSplit = config.model.split(':') - if (modelConfigStringSplit.length != 2) { - throw new Error('TextGenerationInference model string must be colon-delimited and len 2') + public async getEmbedding(content: string, retries = 5): Promise { + try { + const {data, error, response} = await this.client.POST('/embed', { + body: {inputs: content}, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json; charset=utf-8' + } + }) + if (error) { + if (response.status !== 429 || retries < 1) return new Error(error.error) + await sleep(2000) + return this.getEmbedding(content, retries - 1) + } + return data[0]! + } catch (e) { + return e instanceof Error ? e : new Error(e as string) } + } - if (!this.url) throw new Error('TextGenerationInferenceSummarizer model requires url') - const maybeModelId = modelConfigStringSplit[1] - if (!isValidModelId(maybeModelId)) - throw new Error(`TextGenerationInference model id unknown: ${maybeModelId}`) - return modelIdDefinitions[maybeModelId] + protected constructModelParams(modelId: string): EmbeddingModelParams { + const modelParams = modelIdDefinitions[modelId as keyof typeof modelIdDefinitions] + if (!modelParams) throw new Error(`Unknown modelId ${modelId} for TextEmbeddingsInference`) + return modelParams } } diff --git a/packages/embedder/ai_models/TextGenerationInference.ts b/packages/embedder/ai_models/TextGenerationInference.ts index bcf1daa6303..96d7fdde89f 100644 --- a/packages/embedder/ai_models/TextGenerationInference.ts +++ b/packages/embedder/ai_models/TextGenerationInference.ts @@ -1,9 +1,8 @@ import { AbstractGenerationModel, - GenerationModelConfig, GenerationModelParams, GenerationOptions -} from './AbstractModel' +} from './AbstractGenerationModel' import fetchWithRetry from './helpers/fetchWithRetry' const MAX_REQUEST_TIME_S = 3 * 60 @@ -16,13 +15,9 @@ const modelIdDefinitions: Record = { } } -function isValidModelId(object: any): object is ModelId { - return Object.keys(modelIdDefinitions).includes(object) -} - export class TextGenerationInference extends AbstractGenerationModel { - constructor(config: GenerationModelConfig) { - super(config) + constructor(modelId: string, url: string) { + super(modelId, url) } async summarize(content: string, options: GenerationOptions) { @@ -51,7 +46,6 @@ export class TextGenerationInference extends AbstractGenerationModel { } try { - // console.log(`TextGenerationInference.summarize(): summarizing from ${this.url}/generate`) const res = await fetchWithRetry(`${this.url}/generate`, fetchOptions) const json = await res.json() if (!json || !json.generated_text) @@ -62,17 +56,10 @@ export class TextGenerationInference extends AbstractGenerationModel { throw e } } - protected constructModelParams(config: GenerationModelConfig): GenerationModelParams { - const modelConfigStringSplit = config.model.split(':') - if (modelConfigStringSplit.length != 2) { - throw new Error('TextGenerationInference model string must be colon-delimited and len 2') - } - - if (!this.url) throw new Error('TextGenerationInferenceSummarizer model requires url') - const maybeModelId = modelConfigStringSplit[1] - if (!isValidModelId(maybeModelId)) - throw new Error(`TextGenerationInference model id unknown: ${maybeModelId}`) - return modelIdDefinitions[maybeModelId] + protected constructModelParams(modelId: string): GenerationModelParams { + const modelParams = modelIdDefinitions[modelId as keyof typeof modelIdDefinitions] + if (!modelParams) throw new Error(`Unknown modelId ${modelId} for TextGenerationInference`) + return modelParams } } diff --git a/packages/embedder/custom.d.ts b/packages/embedder/custom.d.ts new file mode 100644 index 00000000000..6640974c6a9 --- /dev/null +++ b/packages/embedder/custom.d.ts @@ -0,0 +1,11 @@ +import type {DB} from '../server/postgres/pg' + +export type EmbeddingObjectType = DB['EmbeddingsMetadata']['objectType'] + +export interface MessageToEmbedder { + objectTypes: EmbeddingObjectType[] + startAt?: Date + endAt?: Date + meetingId?: string +} +export type EmbedderOptions = Omit diff --git a/packages/embedder/embedder.ts b/packages/embedder/embedder.ts index f6762013d08..7971ba4f717 100644 --- a/packages/embedder/embedder.ts +++ b/packages/embedder/embedder.ts @@ -1,42 +1,18 @@ -import {Insertable} from 'kysely' import tracer from 'dd-trace' -import Redlock, {RedlockAbortSignal} from 'redlock' - +import EmbedderChannelId from 'parabol-client/shared/gqlIds/EmbedderChannelId' import 'parabol-server/initSentry' -import getKysely from 'parabol-server/postgres/getKysely' -import {DB} from 'parabol-server/postgres/pg' -import {refreshRetroDiscussionTopicsMeta as refreshRetroDiscussionTopicsMeta} from './indexing/retrospectiveDiscussionTopic' -import {orgIdsWithFeatureFlag} from './indexing/orgIdsWithFeatureFlag' -import getModelManager, {ModelManager} from './ai_models/ModelManager' -import {countWords} from './indexing/countWords' -import {createEmbeddingTextFrom} from './indexing/createEmbeddingTextFrom' -import { - selectJobQueueItemById, - selectMetadataByJobQueueId, - updateJobState -} from './indexing/embeddingsTablesOps' -import {selectMetaToQueue} from './indexing/embeddingsTablesOps' -import {insertNewJobs} from './indexing/embeddingsTablesOps' -import {completeJobTxn} from './indexing/embeddingsTablesOps' -import {getRootDataLoader} from './indexing/getRootDataLoader' -import {getRedisClient} from './indexing/getRedisClient' - -/* - * TODO List - * - [ ] implement a clean-up function that re-queues items that haven't transitioned - * to a completed state, or that failed - */ - -export type DBInsert = { - [K in keyof DB]: Insertable -} - -const POLLING_PERIOD_SEC = 60 // How often do we try to grab the lock and re-index? -const Q_MAX_LENGTH = 100 // How many EmbeddingIndex items do we batch in redis? -const WORD_COUNT_TO_TOKEN_RATIO = 3.0 / 2 // We multiple the word count by this to estimate token count - -const {AI_EMBEDDER_ENABLED} = process.env -const {SERVER_ID} = process.env +import {Logger} from 'parabol-server/utils/Logger' +import RedisInstance from 'parabol-server/utils/RedisInstance' +import {Tuple} from '../client/types/generics' +import RedisStream from '../gql-executor/RedisStream' +import {EmbeddingsJobQueueStream} from './EmbeddingsJobQueueStream' +import {addEmbeddingsMetadata} from './addEmbeddingsMetadata' +import getModelManager from './ai_models/ModelManager' +import {MessageToEmbedder} from './custom' +import {establishPrimaryEmbedder} from './establishPrimaryEmbedder' +import {importHistoricalMetadata} from './importHistoricalMetadata' +import {mergeAsyncIterators} from './mergeAsyncIterators' +import {resetStalledJobs} from './resetStalledJobs' tracer.init({ service: `embedder`, @@ -46,207 +22,89 @@ tracer.init({ }) tracer.use('pg') -const refreshMetadata = async () => { - const dataLoader = getRootDataLoader() - await refreshRetroDiscussionTopicsMeta(dataLoader) - // In the future, other sorts of objects to index could be added here... +const parseEmbedderMessage = (message: string): MessageToEmbedder => { + const {startAt, endAt, ...input} = JSON.parse(message) + return { + ...input, + startAt: startAt ? new Date(startAt) : undefined, + endAt: endAt ? new Date(endAt) : undefined + } } -const maybeQueueMetadataItems = async (modelManager: ModelManager) => { - const redisClient = getRedisClient() - const queueLength = await redisClient.zcard('embedder:queue') - if (queueLength >= Q_MAX_LENGTH) return - const itemCountToQueue = Q_MAX_LENGTH - queueLength - const modelTables = modelManager.embeddingModels.map((m) => m.tableName) - const orgIds = await orgIdsWithFeatureFlag() - - // For each configured embedding model, select rows from EmbeddingsMetadata - // that haven't been calculated nor exist in the EmbeddingsJobQueue yet - // - // Notes: - // * `em.models @> ARRAY[v.model]` is an indexed query - // * I don't love all overrides, I wish there was a better way - // see: https://github.com/kysely-org/kysely/issues/872 - - const batchToQueue = await selectMetaToQueue(modelTables, orgIds, itemCountToQueue) - if (!batchToQueue.length) { - console.log(`embedder: no new items to queue`) +const run = async () => { + const SERVER_ID = process.env.SERVER_ID + if (!SERVER_ID) throw new Error('env.SERVER_ID is required') + const embedderChannel = EmbedderChannelId.join(SERVER_ID) + const NUM_WORKERS = parseInt(process.env.AI_EMBEDDER_WORKERS!) + if (!(NUM_WORKERS > 0)) { + Logger.log('env.AI_EMBEDDER_WORKERS is < 0. Embedder will not run.') return } - const ejqHash: { - [key: string]: { - refUpdatedAt: Date - } - } = {} - const makeKey = (item: {objectType: string; refId: string}) => `${item.objectType}:${item.refId}` - - const ejqValues = batchToQueue.map((item) => { - ejqHash[makeKey(item)] = { - refUpdatedAt: item.refUpdatedAt - } - return { - objectType: item.objectType, - refId: item.refId as string, - model: item.model, - state: 'queued' as const - } - }) - - const ejqRows = await insertNewJobs(ejqValues) - - ejqRows.forEach((item) => { - const {refUpdatedAt} = ejqHash[makeKey(item)]! - const score = new Date(refUpdatedAt).getTime() - redisClient.zadd('embedder:queue', score, item.id) - }) - - console.log(`embedder: queued ${batchToQueue.length} items`) -} - -const dequeueAndEmbedUntilEmpty = async (modelManager: ModelManager) => { - const dataLoader = getRootDataLoader() - const redisClient = getRedisClient() - while (true) { - const maybeRedisQItem = await redisClient.zpopmax('embedder:queue', 1) - if (maybeRedisQItem.length < 2) return // Q is empty, all done! - - const [id, _] = maybeRedisQItem - if (!id) { - console.log(`embedder: de-queued undefined item from embedder:queue`) - continue - } - const jobQueueId = parseInt(id, 10) - const jobQueueItem = await selectJobQueueItemById(jobQueueId) - if (!jobQueueItem) { - console.log(`embedder: unable to fetch EmbeddingsJobQueue.id = ${id}`) - continue - } - - const metadata = await selectMetadataByJobQueueId(jobQueueId) - if (!metadata) { - await updateJobState(jobQueueId, 'failed', { - stateMessage: `unable to fetch metadata by EmbeddingsJobQueue.id = ${id}` - }) - continue - } - - let fullText = metadata?.fullText - try { - if (!fullText) { - fullText = await createEmbeddingTextFrom(jobQueueItem, dataLoader) - } - } catch (e) { - await updateJobState(jobQueueId, 'failed', { - stateMessage: `unable to create embedding text: ${e}` - }) - continue - } + const redis = new RedisInstance(`embedder_${SERVER_ID}`) + const primaryLock = await establishPrimaryEmbedder(redis) + const modelManager = getModelManager() + let streams: AsyncIterableIterator | undefined = undefined + const kill = () => { + primaryLock?.release() + streams?.return?.() + process.exit() + } + process.on('SIGTERM', kill) + process.on('SIGINT', kill) + if (primaryLock) { + // only 1 worker needs to perform these on startup + await modelManager.maybeCreateTables() + await importHistoricalMetadata() + resetStalledJobs() + } - const wordCount = countWords(fullText) + const onMessage = async (_channel: string, message: string) => { + const parsedMessage = parseEmbedderMessage(message) + await addEmbeddingsMetadata(parsedMessage) + } - const embeddingModel = modelManager.embeddingModelsMapByTable[jobQueueItem.model] - if (!embeddingModel) { - await updateJobState(jobQueueId, 'failed', { - stateMessage: `embedding model ${jobQueueItem.model} not available` - }) - continue - } - const itemKey = `${jobQueueItem.objectType}:${jobQueueItem.refId}` - const modelTable = embeddingModel.tableName + // subscribe to consumer group + try { + await redis.xgroup( + 'CREATE', + 'embedMetadataStream', + 'embedMetadataConsumerGroup', + '$', + 'MKSTREAM' + ) + } catch (e) { + // stream already exists + } - let embedText = fullText - const maxInputTokens = embeddingModel.maxInputTokens - // we're using word count as an appoximation of tokens - if (wordCount * WORD_COUNT_TO_TOKEN_RATIO > maxInputTokens) { - try { - const generator = modelManager.generationModels[0] // use 1st generator - if (!generator) throw new Error(`Generator unavailable`) - console.log(`embedder: ...summarizing ${itemKey} for ${modelTable}`) - embedText = await generator.summarize(fullText, {maxNewTokens: maxInputTokens}) - } catch (e) { - await updateJobState(jobQueueId, 'failed', { - stateMessage: `unable to summarize long embed text: ${e}` - }) + const messageStream = new RedisStream( + 'embedMetadataStream', + 'embedMetadataConsumerGroup', + embedderChannel + ) + + // Assume 3 workers for type safety, but it doesn't really matter at runtime + const jobQueueStreams = Array.from( + {length: NUM_WORKERS}, + () => new EmbeddingsJobQueueStream() + ) as Tuple + + Logger.log(`\n⚡⚡⚡️️ Server ID: ${SERVER_ID}. Embedder is ready ⚡⚡⚡️️️`) + + streams = mergeAsyncIterators([messageStream, ...jobQueueStreams]) + for await (const [idx, message] of streams) { + switch (idx) { + case 0: + onMessage('', message) + continue + default: + Logger.log(`Worker ${idx} finished job ${message.id}`) continue - } - } - // console.log(`embedText: ${embedText}`) - - let embeddingVector: number[] - try { - embeddingVector = await embeddingModel.getEmbedding(embedText) - } catch (e) { - await updateJobState(jobQueueId, 'failed', { - stateMessage: `unable to get embeddings: ${e}` - }) - continue } - - // complete job, do the following atomically - // (1) update EmbeddingsMetadata to reflect model completion - // (2) upsert model table row with embedding - // (3) delete EmbeddingsJobQueue row - await completeJobTxn(modelTable, jobQueueId, metadata, fullText, embedText, embeddingVector) - console.log(`embedder: completed ${itemKey} -> ${modelTable}`) } -} - -const tick = async (modelManager: ModelManager) => { - console.log(`embedder: tick`) - const redisClient = getRedisClient() - const redlock = new Redlock([redisClient], { - driftFactor: 0.01, - retryCount: 10, - retryDelay: 250, - retryJitter: 50, - automaticExtensionThreshold: 500 - }) - - await redlock - .using(['embedder:lock'], 10000, async (signal: RedlockAbortSignal) => { - console.log(`embedder: acquired index queue lock`) - // N.B. one of the many benefits of using redlock is the using() interface - // will automatically extend the lock if these operations exceed the - // original redis timeout time - await refreshMetadata() - await maybeQueueMetadataItems(modelManager) - - if (signal.aborted) { - // Not certain which conditions this would happen, it would - // happen after operations took place, so nothing much to do here. - console.log('embedder: lock was lost!') - } - }) - .catch((err: string) => { - // Handle errors (including lock acquisition errors) - console.error('embedder: an error occurred ', err) - }) - console.log('embedder: index queue lock released') - - // get the highest priority item and embed it - await dequeueAndEmbedUntilEmpty(modelManager) - - setTimeout(() => tick(modelManager), POLLING_PERIOD_SEC * 1000) -} - -function parseEnvBoolean(envVarValue: string | undefined): boolean { - return envVarValue === 'true' -} -const run = async () => { - console.log(`embedder: run()`) - const embedderEnabled = parseEnvBoolean(AI_EMBEDDER_ENABLED) - const modelManager = getModelManager() - if (embedderEnabled && modelManager) { - const pg = getKysely() - await modelManager.maybeCreateTables(pg) - console.log(`\n⚡⚡⚡️️ Server ID: ${SERVER_ID}. Embedder is ready ⚡⚡⚡️️️`) - tick(modelManager) - } else { - console.log(`embedder: no valid configuration (check AI_EMBEDDER_ENABLED in .env)`) - // exit - } + // On graceful shutdown + Logger.log('Streaming Complete. Goodbye!') } run() diff --git a/packages/embedder/establishPrimaryEmbedder.ts b/packages/embedder/establishPrimaryEmbedder.ts new file mode 100644 index 00000000000..72f349d655f --- /dev/null +++ b/packages/embedder/establishPrimaryEmbedder.ts @@ -0,0 +1,17 @@ +import ms from 'ms' +import RedisInstance from 'parabol-server/utils/RedisInstance' +import Redlock from 'redlock' + +export const establishPrimaryEmbedder = async (redis: RedisInstance) => { + const redlock = new Redlock([redis], {retryCount: 0}) + const MAX_TIME_BETWEEN_WORKER_STARTUPS = ms('5s') + try { + const primaryWorkerLock = await redlock.acquire( + [`embedder_isPrimary_${process.env.npm_package_version}`], + MAX_TIME_BETWEEN_WORKER_STARTUPS + ) + return primaryWorkerLock + } catch { + return undefined + } +} diff --git a/packages/embedder/importHistoricalMetadata.ts b/packages/embedder/importHistoricalMetadata.ts new file mode 100644 index 00000000000..0b805f18888 --- /dev/null +++ b/packages/embedder/importHistoricalMetadata.ts @@ -0,0 +1,16 @@ +import {EmbeddingObjectType} from './custom' +import {importHistoricalRetrospectiveDiscussionTopic} from './importHistoricalRetrospectiveDiscussionTopic' + +export const importHistoricalMetadata = async () => { + const OBJECT_TYPES: EmbeddingObjectType[] = ['retrospectiveDiscussionTopic'] + return Promise.all( + OBJECT_TYPES.map(async (objectType) => { + switch (objectType) { + case 'retrospectiveDiscussionTopic': + return importHistoricalRetrospectiveDiscussionTopic() + default: + throw new Error(`Invalid object type: ${objectType}`) + } + }) + ) +} diff --git a/packages/embedder/importHistoricalRetrospectiveDiscussionTopic.ts b/packages/embedder/importHistoricalRetrospectiveDiscussionTopic.ts new file mode 100644 index 00000000000..2469327a18a --- /dev/null +++ b/packages/embedder/importHistoricalRetrospectiveDiscussionTopic.ts @@ -0,0 +1,37 @@ +import getKysely from 'parabol-server/postgres/getKysely' +import {Logger} from 'parabol-server/utils/Logger' +import {addEmbeddingsMetadataForRetrospectiveDiscussionTopic} from './addEmbeddingsMetadataForRetrospectiveDiscussionTopic' + +// Check to see if the oldest discussion topic exists in the metadata table +// If not, get the date of the oldest discussion topic in the metadata table and import all items before that date +export const importHistoricalRetrospectiveDiscussionTopic = async () => { + const pg = getKysely() + const isEarliestMetadataImported = await pg + .selectFrom('EmbeddingsMetadata') + .select('id') + .where(({eb, selectFrom}) => + eb( + 'EmbeddingsMetadata.refId', + '=', + selectFrom('Discussion') + .select('Discussion.id') + .where('discussionTopicType', '=', 'reflectionGroup') + .orderBy(['createdAt', 'id']) + .limit(1) + ) + ) + .limit(1) + .executeTakeFirst() + + if (isEarliestMetadataImported) return + const earliestImportedDiscussion = await pg + .selectFrom('EmbeddingsMetadata') + .select(['id', 'refUpdatedAt', 'refId']) + .where('objectType', '=', 'retrospectiveDiscussionTopic') + .orderBy('refUpdatedAt') + .limit(1) + .executeTakeFirst() + const endAt = earliestImportedDiscussion?.refUpdatedAt ?? undefined + Logger.log(`Importing discussion history up to ${endAt || 'now'}`) + return addEmbeddingsMetadataForRetrospectiveDiscussionTopic({endAt}) +} diff --git a/packages/embedder/indexing/countWords.ts b/packages/embedder/indexing/countWords.ts deleted file mode 100644 index 75dae3effa2..00000000000 --- a/packages/embedder/indexing/countWords.ts +++ /dev/null @@ -1,17 +0,0 @@ -export function countWords(text: string) { - let count = 0 - let inWord = false - - for (const char of text) { - if (/\w/.test(char)) { - if (!inWord) { - count++ - inWord = true - } - } else { - inWord = false - } - } - - return count -} diff --git a/packages/embedder/indexing/createEmbeddingTextFrom.ts b/packages/embedder/indexing/createEmbeddingTextFrom.ts index 9d6e66b60e7..9fb3cceda80 100644 --- a/packages/embedder/indexing/createEmbeddingTextFrom.ts +++ b/packages/embedder/indexing/createEmbeddingTextFrom.ts @@ -1,15 +1,17 @@ import {Selectable} from 'kysely' import {DB} from 'parabol-server/postgres/pg' -import {DataLoaderWorker} from 'parabol-server/graphql/graphql' -import {createText as createTextFromRetrospectiveDiscussionTopic} from './retrospectiveDiscussionTopic' +import RootDataLoader from 'parabol-server/dataloader/RootDataLoader' +import {createTextFromRetrospectiveDiscussionTopic} from './retrospectiveDiscussionTopic' export const createEmbeddingTextFrom = async ( - item: Selectable, - dataLoader: DataLoaderWorker -): Promise => { - switch (item.objectType) { + embeddingsMetadata: Selectable, + dataLoader: RootDataLoader +) => { + switch (embeddingsMetadata.objectType) { case 'retrospectiveDiscussionTopic': - return createTextFromRetrospectiveDiscussionTopic(item, dataLoader) + return createTextFromRetrospectiveDiscussionTopic(embeddingsMetadata.refId, dataLoader) + default: + throw new Error(`Unexcepted objectType: ${embeddingsMetadata.objectType}`) } } diff --git a/packages/embedder/indexing/embeddingsTablesOps.ts b/packages/embedder/indexing/embeddingsTablesOps.ts deleted file mode 100644 index c74eb709708..00000000000 --- a/packages/embedder/indexing/embeddingsTablesOps.ts +++ /dev/null @@ -1,198 +0,0 @@ -import {Insertable, Selectable, Updateable, sql} from 'kysely' -import getKysely from 'parabol-server/postgres/getKysely' -import {DB} from 'parabol-server/postgres/pg' -import {DBInsert} from '../embedder' -import {RawBuilder} from 'kysely' -import numberVectorToString from './numberVectorToString' - -function unnestedArray(maybeArray: T[] | T): RawBuilder { - let a: T[] = Array.isArray(maybeArray) ? maybeArray : [maybeArray] - return sql`unnest(ARRAY[${sql.join(a)}]::varchar[])` -} - -export const selectJobQueueItemById = async ( - id: number -): Promise | undefined> => { - const pg = getKysely() - return pg.selectFrom('EmbeddingsJobQueue').selectAll().where('id', '=', id).executeTakeFirst() -} -export const selectMetadataByJobQueueId = async ( - id: number -): Promise | undefined> => { - const pg = getKysely() - return pg - .selectFrom('EmbeddingsMetadata as em') - .selectAll() - .leftJoin('EmbeddingsJobQueue as ejq', (join) => - join.onRef('em.objectType', '=', 'ejq.objectType').onRef('em.refId', '=', 'ejq.refId') - ) - .where('ejq.id', '=', id) - .executeTakeFirstOrThrow() -} - -// For each configured embedding model, select rows from EmbeddingsMetadata -// that haven't been calculated nor exist in the EmbeddingsJobQueue yet -// -// Notes: -// * `em.models @> ARRAY[v.model]` is an indexed query -// * I don't love all overrides, I wish there was a better way -// see: https://github.com/kysely-org/kysely/issues/872 -export async function selectMetaToQueue( - configuredModels: string[], - orgIds: any[], - itemCountToQueue: number -) { - const pg = getKysely() - const maybeMetaToQueue = (await pg - .selectFrom('EmbeddingsMetadata as em') - .selectAll('em') - .leftJoinLateral(unnestedArray(configuredModels).as('model'), (join) => join.onTrue()) - .leftJoin('Team as t', 'em.teamId', 't.id') - .select('model' as any) - .where(({eb, not, or, and, exists, selectFrom}) => - and([ - or([ - not(eb('em.models', '@>', sql`ARRAY[${sql.ref('model')}]::varchar[]` as any) as any), - eb('em.models' as any, 'is', null) - ]), - not( - exists( - selectFrom('EmbeddingsJobQueue as ejq') - .select('ejq.id') - .whereRef('em.objectType', '=', 'ejq.objectType') - .whereRef('em.refId', '=', 'ejq.refId') - .whereRef('ejq.model', '=', 'model' as any) - ) - ), - eb('t.orgId', 'in', orgIds) - ]) - ) - .limit(itemCountToQueue) - .execute()) as unknown as Selectable[] - - type MetadataToQueue = Selectable< - Omit & { - refId: NonNullable - } & {model: string} - > - - return maybeMetaToQueue.filter( - (item) => item.refId !== null && item.refId !== undefined - ) as MetadataToQueue[] -} - -export const updateJobState = async ( - id: number, - state: Updateable['state'], - jobQueueFields: Updateable = {} -) => { - const pg = getKysely() - const jobQueueColumns: Updateable = { - ...jobQueueFields, - state - } - if (state === 'failed') console.log(`embedder: failed job ${id}, ${jobQueueFields.stateMessage}`) - return pg - .updateTable('EmbeddingsJobQueue') - .set(jobQueueColumns) - .where('id', '=', id) - .executeTakeFirstOrThrow() -} - -export function insertNewJobs(ejqValues: Insertable[]) { - const pg = getKysely() - return pg - .insertInto('EmbeddingsJobQueue') - .values(ejqValues) - .returning(['id', 'objectType', 'refId']) - .execute() -} - -// complete job, do the following atomically -// (1) update EmbeddingsMetadata to reflect model completion -// (2) upsert model table row with embedding -// (3) delete EmbeddingsJobQueue row -export function completeJobTxn( - modelTable: string, - jobQueueId: number, - metadata: Updateable, - fullText: string, - embedText: string, - embeddingVector: number[] -) { - const pg = getKysely() - return pg.transaction().execute(async (trx) => { - // get fields to update correct metadata row - const jobQueueItem = await trx - .selectFrom('EmbeddingsJobQueue') - .select(['objectType', 'refId', 'model']) - .where('id', '=', jobQueueId) - .executeTakeFirstOrThrow() - - // (1) update metadata row - const metadataColumnsToUpdate: { - models: RawBuilder - fullText?: string | null | undefined - } = { - // update models as a set - models: sql`( -SELECT array_agg(DISTINCT value) -FROM ( - SELECT unnest(COALESCE("models", '{}')) AS value - UNION - SELECT unnest(ARRAY[${modelTable}]::VARCHAR[]) AS value -) AS combined_values -)` - } - - if (metadata?.fullText !== fullText) { - metadataColumnsToUpdate.fullText = fullText - } - - const updatedMetadata = await trx - .updateTable('EmbeddingsMetadata') - .set(metadataColumnsToUpdate) - .where('objectType', '=', jobQueueItem.objectType) - .where('refId', '=', jobQueueItem.refId) - .returning(['id']) - .executeTakeFirstOrThrow() - - // (2) upsert into model table - await trx - .insertInto(modelTable as any) - .values({ - embedText: fullText !== embedText ? embedText : null, - embedding: numberVectorToString(embeddingVector), - embeddingsMetadataId: updatedMetadata.id - }) - .onConflict((oc) => - oc.column('id').doUpdateSet((eb) => ({ - embedText: eb.ref('excluded.embedText'), - embeddingsMetadataId: eb.ref('excluded.embeddingsMetadataId') - })) - ) - .executeTakeFirstOrThrow() - - // (3) delete completed job queue item - return await trx - .deleteFrom('EmbeddingsJobQueue') - .where('id', '=', jobQueueId) - .executeTakeFirstOrThrow() - }) -} -export async function upsertEmbeddingsMetaRows( - embeddingsMetaRows: DBInsert['EmbeddingsMetadata'][] -) { - const pg = getKysely() - return pg - .insertInto('EmbeddingsMetadata') - .values(embeddingsMetaRows) - .onConflict((oc) => - oc.columns(['objectType', 'refId']).doUpdateSet((eb) => ({ - objectType: eb.ref('excluded.objectType'), - refId: eb.ref('excluded.refId'), - refUpdatedAt: eb.ref('excluded.refUpdatedAt') - })) - ) - .execute() -} diff --git a/packages/embedder/indexing/failJob.ts b/packages/embedder/indexing/failJob.ts new file mode 100644 index 00000000000..17d293b49a9 --- /dev/null +++ b/packages/embedder/indexing/failJob.ts @@ -0,0 +1,17 @@ +import getKysely from 'parabol-server/postgres/getKysely' +import {Logger} from 'parabol-server/utils/Logger' + +export const failJob = async (jobId: number, stateMessage: string, retryAfter?: Date | null) => { + const pg = getKysely() + Logger.log(`embedder: failed job ${jobId}, ${stateMessage}`) + await pg + .updateTable('EmbeddingsJobQueue') + .set((eb) => ({ + state: 'failed', + stateMessage, + retryCount: eb('retryCount', '+', 1), + retryAfter: retryAfter || null + })) + .where('id', '=', jobId) + .executeTakeFirstOrThrow() +} diff --git a/packages/embedder/indexing/getRedisClient.ts b/packages/embedder/indexing/getRedisClient.ts deleted file mode 100644 index 7aaf65be33c..00000000000 --- a/packages/embedder/indexing/getRedisClient.ts +++ /dev/null @@ -1,11 +0,0 @@ -import RedisInstance from 'parabol-server/utils/RedisInstance' - -const {SERVER_ID} = process.env - -let redisClient: RedisInstance -export const getRedisClient = () => { - if (!redisClient) { - redisClient = new RedisInstance(`embedder-${SERVER_ID}`) - } - return redisClient -} diff --git a/packages/embedder/indexing/getRootDataLoader.ts b/packages/embedder/indexing/getRootDataLoader.ts deleted file mode 100644 index 304c0c01058..00000000000 --- a/packages/embedder/indexing/getRootDataLoader.ts +++ /dev/null @@ -1,10 +0,0 @@ -import getDataLoader from 'parabol-server/graphql/getDataLoader' -import {DataLoaderWorker} from 'parabol-server/graphql/graphql' - -let rootDataLoader: DataLoaderWorker -export const getRootDataLoader = () => { - if (!rootDataLoader) { - rootDataLoader = getDataLoader() as DataLoaderWorker - } - return rootDataLoader -} diff --git a/packages/embedder/indexing/retrospectiveDiscussionTopic.ts b/packages/embedder/indexing/retrospectiveDiscussionTopic.ts index f31aab74cc2..03af3b510ba 100644 --- a/packages/embedder/indexing/retrospectiveDiscussionTopic.ts +++ b/packages/embedder/indexing/retrospectiveDiscussionTopic.ts @@ -1,26 +1,8 @@ -import {Selectable} from 'kysely' -import prettier from 'prettier' - -import getRethink, {RethinkSchema} from 'parabol-server/database/rethinkDriver' -import {DataLoaderWorker} from 'parabol-server/graphql/graphql' -import getKysely from 'parabol-server/postgres/getKysely' -import {DB} from 'parabol-server/postgres/pg' - +import {RethinkSchema} from 'parabol-server/database/rethinkDriver' import Comment from 'parabol-server/database/types/Comment' -import DiscussStage from 'parabol-server/database/types/DiscussStage' -import MeetingRetrospective, { - isMeetingRetrospective -} from 'parabol-server/database/types/MeetingRetrospective' - -import {upsertEmbeddingsMetaRows} from './embeddingsTablesOps' -import {AnyMeeting} from 'parabol-server/postgres/types/Meeting' - -const BATCH_SIZE = 1000 - -export interface EmbeddingsJobQueueRetrospectiveDiscussionTopic - extends Omit { - objectType: 'retrospectiveDiscussionTopic' -} +import {isMeetingRetrospective} from 'parabol-server/database/types/MeetingRetrospective' +import RootDataLoader from 'parabol-server/dataloader/RootDataLoader' +import prettier from 'prettier' // Here's a generic reprentation of the text generated here: @@ -39,109 +21,14 @@ export interface EmbeddingsJobQueueRetrospectiveDiscussionTopic const IGNORE_COMMENT_USER_IDS = ['parabolAIUser'] -const pg = getKysely() - -export async function refreshRetroDiscussionTopicsMeta(dataLoader: DataLoaderWorker) { - const r = await getRethink() - const {createdAt: newestMeetingDate} = (await r - .table('NewMeeting') - .max({index: 'createdAt'}) - .run()) as unknown as RethinkSchema['NewMeeting']['type'] - const {createdAt: oldestMeetingDate} = (await r - .table('NewMeeting') - .min({index: 'createdAt'}) - .run()) as unknown as RethinkSchema['NewMeeting']['type'] - - const {newestMetaDate} = (await pg - .selectFrom('EmbeddingsMetadata') - .select(pg.fn.max('refUpdatedAt').as('newestMetaDate')) - .where('objectType', '=', 'retrospectiveDiscussionTopic') - .executeTakeFirst()) ?? {newestMetaDate: null} - let startDateTime = newestMetaDate || oldestMeetingDate - - if (startDateTime.getTime() === newestMeetingDate.getTime()) return - - console.log( - `refreshRetroDiscussionTopicsMeta(): ` + - `will consider adding items from ${startDateTime.toISOString()} to ` + - `${newestMeetingDate.toISOString()}` - ) - - let totalAdded = 0 - do { - // Process history in batches. - // - // N.B. We add historical meetings to the EmbeddingsMetadata table here. - // This query will intentionally miss meetings that haven't been completed - // (`summarySentAt` is null). These meetings will need to be added to the - // EmbeddingsMetadata table by a hook that runs when the meetings complete. - const {maxCreatedAt, completedNewMeetings} = await r - .table('NewMeeting') - .between(startDateTime, newestMeetingDate, {rightBound: 'closed', index: 'createdAt'}) - .orderBy({index: 'createdAt'}) - .limit(BATCH_SIZE) - .coerceTo('array') - .do((rows: any) => ({ - maxCreatedAt: r.expr(rows).max('createdAt')('createdAt'), // Then find the max createdAt value - completedNewMeetings: r.expr(rows).filter((r: any) => - r('meetingType') - .eq('retrospective') - .and( - r('endedAt').gt(0), - r - .hasFields('phases') - .and(r('phases').count().gt(0)) - .and( - r('phases') - .filter((phase: any) => phase('phaseType').eq('discuss')) - .filter((phase: any) => - phase.hasFields('stages').and(phase('stages').count().gt(0)) - ) - .count() - .gt(0) - ) - ) - ) - })) - .run() - const embeddingsMetaRows = ( - await Promise.all( - completedNewMeetings.map((m: AnyMeeting) => - newRetroDiscussionTopicsFromNewMeeting(m, dataLoader) - ) - ) - ).flat() - if (embeddingsMetaRows.length > 0) { - await upsertEmbeddingsMetaRows(embeddingsMetaRows) - totalAdded += embeddingsMetaRows.length - console.log( - `refreshRetroDiscussionTopicsMeta(): synced to ${maxCreatedAt.toISOString()}, added` + - ` ${embeddingsMetaRows.length} retrospectiveDiscussionTopics` - ) - } - - // N.B. In the unlikely event that we have >=BATCH_SIZE meetings that end at _exactly_ - // the same timetsamp, this will loop forever. - if ( - startDateTime.getTime() === newestMeetingDate.getTime() && - completedNewMeetings.length < BATCH_SIZE - ) - break - startDateTime = maxCreatedAt - } while (true) - - console.log( - `refreshRetroDiscussionTopicsMeta(): added ${totalAdded} total retrospectiveDiscussionTopics` - ) -} - -async function getPreferredNameByUserId(userId: string, dataLoader: DataLoaderWorker) { +async function getPreferredNameByUserId(userId: string, dataLoader: RootDataLoader) { + if (!userId) return 'Unknown' const user = await dataLoader.get('users').load(userId) return !user ? 'Unknown' : user.preferredName } async function formatThread( - dataLoader: DataLoaderWorker, + dataLoader: RootDataLoader, comments: Comment[], parentId: string | null = null, depth = 0 @@ -156,9 +43,7 @@ async function formatThread( const indent = ' '.repeat(depth + 1) const author = comment.isAnonymous ? 'Anonymous' - : comment.createdBy - ? await getPreferredNameByUserId(comment.createdBy, dataLoader) - : 'Unknown' + : await getPreferredNameByUserId(comment.createdBy, dataLoader) const how = depth === 0 ? 'wrote' : 'replied' const content = comment.plaintextContent const formattedPost = `${indent}- ${author} ${how}, "${content}"\n` @@ -173,60 +58,47 @@ async function formatThread( return formattedComments.join('') } -export const createTextFromNewMeetingDiscussionStage = async ( - newMeeting: MeetingRetrospective, - stageId: string, - dataLoader: DataLoaderWorker, +export const createTextFromRetrospectiveDiscussionTopic = async ( + discussionId: string, + dataLoader: RootDataLoader, textForReranking: boolean = false ) => { - if (!newMeeting) throw 'newMeeting is undefined' - if (!isMeetingRetrospective(newMeeting)) throw 'newMeeting is not retrospective' - if (!newMeeting.templateId) throw 'template is undefined' - const template = await dataLoader.get('meetingTemplates').load(newMeeting.templateId) - if (!template) throw 'template is undefined' - const discussPhase = newMeeting.phases.find((phase) => phase.phaseType === 'discuss') - if (!discussPhase) throw 'newMeeting discuss phase is undefined' - if (!discussPhase.stages) throw 'newMeeting discuss phase has no stages' - const discussStage = discussPhase.stages.find((stage) => stage.id === stageId) as DiscussStage - if (!discussStage) throw 'newMeeting discuss stage not found' - const {summary: discussionSummary} = discussStage.discussionId - ? (await dataLoader.get('discussions').load(discussStage.discussionId)) ?? {summary: null} - : {summary: null} - const r = await getRethink() - if (!discussStage.reflectionGroupId) throw 'newMeeting discuss stage has no reflectionGroupId' - const reflectionGroup = await r - .table('RetroReflectionGroup') - .get(discussStage.reflectionGroupId) - .run() - if (!reflectionGroup.id) throw 'newMeeting reflectionGroup has no id' - const reflections = await r - .table('RetroReflection') - .getAll(reflectionGroup.id, {index: 'reflectionGroupId'}) - .run() + const discussion = await dataLoader.get('discussions').load(discussionId) + if (!discussion) throw new Error(`Discussion not found: ${discussionId}`) + const {discussionTopicId: reflectionGroupId, meetingId, summary: discussionSummary} = discussion + const [newMeeting, reflectionGroup, reflections] = await Promise.all([ + dataLoader.get('newMeetings').load(meetingId), + dataLoader.get('retroReflectionGroups').load(reflectionGroupId), + dataLoader.get('retroReflectionsByGroupId').load(reflectionGroupId) + ]) + if (!isMeetingRetrospective(newMeeting)) throw new Error('Meeting is not a retro') + const {templateId} = newMeeting + const promptIds = [...new Set(reflections.map((r) => r.promptId))] + const [template, ...prompts] = await Promise.all([ + dataLoader.get('meetingTemplates').loadNonNull(templateId), + ...promptIds.map((promptId) => dataLoader.get('reflectPrompts').load(promptId)) + ]) + let markdown = '' - if (!textForReranking) + if (!textForReranking) { markdown = - `A topic "${reflectionGroup.title}" was discussed during ` + + `A topic "${reflectionGroup?.title ?? ''}" was discussed during ` + `the meeting "${newMeeting.name}" that followed the "${template.name}" template.\n` + `\n` - const prompts = await dataLoader.get('reflectPrompts').loadMany(promptIds) + } + for (const prompt of prompts) { - if (!prompt || prompt instanceof Error) continue if (!textForReranking) { markdown += `Participants were prompted with, "${prompt.question}` if (prompt.description) markdown += `: ${prompt.description}` markdown += `".\n` } - if (newMeeting.disableAnonymity) { - for (const reflection of reflections.filter((r) => r.promptId === prompt.id)) { - const author = await getPreferredNameByUserId(reflection.creatorId, dataLoader) - markdown += ` - ${author} wrote, "${reflection.plaintextContent}"\n` - } - } else { - for (const reflection of reflections.filter((r) => r.promptId === prompt.id)) { - markdown += ` - Anonymous wrote, "${reflection.plaintextContent}"\n` - } + for (const reflection of reflections.filter((r) => r.promptId === prompt.id)) { + const author = newMeeting.disableAnonymity + ? await getPreferredNameByUserId(reflection.creatorId, dataLoader) + : 'Anonymous' + markdown += ` - ${author} wrote, "${reflection.plaintextContent}"\n` } markdown += `\n` } @@ -251,7 +123,7 @@ export const createTextFromNewMeetingDiscussionStage = async ( if (discussionSummary) { markdown += `Further discussion was made. ` + ` ${discussionSummary}` } else { - const comments = await dataLoader.get('commentsByDiscussionId').load(stageId) + const comments = await dataLoader.get('commentsByDiscussionId').load(discussionId) const sortedComments = comments .map((comment) => { @@ -267,8 +139,8 @@ export const createTextFromNewMeetingDiscussionStage = async ( if (a.threadParentId === b.threadParentId) { return a.threadSortOrder - b.threadSortOrder } - if (a.threadParentId == null) return 1 - if (b.threadParentId == null) return -1 + if (!a.threadParentId) return 1 + if (!b.threadParentId) return -1 return a.threadParentId > b.threadParentId ? 1 : -1 }) as Comment[] @@ -291,25 +163,9 @@ export const createTextFromNewMeetingDiscussionStage = async ( return markdown } -export const createText = async ( - item: Selectable, - dataLoader: DataLoaderWorker -): Promise => { - if (!item.refId) throw 'refId is undefined' - const [newMeetingId, discussionId] = item.refId.split(':') - if (!newMeetingId) throw new Error('newMeetingId cannot be undefined') - if (!discussionId) throw new Error('discussionId cannot be undefined') - const newMeeting = await dataLoader.get('newMeetings').load(newMeetingId) - return createTextFromNewMeetingDiscussionStage( - newMeeting as MeetingRetrospective, - discussionId, - dataLoader - ) -} - export const newRetroDiscussionTopicsFromNewMeeting = async ( newMeeting: RethinkSchema['NewMeeting']['type'], - dataLoader: DataLoaderWorker + dataLoader: RootDataLoader ) => { const discussPhase = newMeeting.phases.find((phase) => phase.phaseType === 'discuss') const orgId = (await dataLoader.get('teams').load(newMeeting.teamId))?.orgId diff --git a/packages/embedder/iso6393To1.ts b/packages/embedder/iso6393To1.ts new file mode 100644 index 00000000000..f1e470e1232 --- /dev/null +++ b/packages/embedder/iso6393To1.ts @@ -0,0 +1,195 @@ +import {ValueOf} from '../client/types/generics' + +/** + * Map of ISO 639-3 codes to ISO 639-1 codes. + * + * @type {Record} + */ +export const iso6393To1 = { + aar: 'aa', + abk: 'ab', + afr: 'af', + aka: 'ak', + amh: 'am', + ara: 'ar', + arg: 'an', + asm: 'as', + ava: 'av', + ave: 'ae', + aym: 'ay', + aze: 'az', + bak: 'ba', + bam: 'bm', + bel: 'be', + ben: 'bn', + bis: 'bi', + bod: 'bo', + bos: 'bs', + bre: 'br', + bul: 'bg', + cat: 'ca', + ces: 'cs', + cha: 'ch', + che: 'ce', + chu: 'cu', + chv: 'cv', + cor: 'kw', + cos: 'co', + cre: 'cr', + cym: 'cy', + dan: 'da', + deu: 'de', + div: 'dv', + dzo: 'dz', + ell: 'el', + eng: 'en', + epo: 'eo', + est: 'et', + eus: 'eu', + ewe: 'ee', + fao: 'fo', + fas: 'fa', + fij: 'fj', + fin: 'fi', + fra: 'fr', + fry: 'fy', + ful: 'ff', + gla: 'gd', + gle: 'ga', + glg: 'gl', + glv: 'gv', + grn: 'gn', + guj: 'gu', + hat: 'ht', + hau: 'ha', + hbs: 'sh', + heb: 'he', + her: 'hz', + hin: 'hi', + hmo: 'ho', + hrv: 'hr', + hun: 'hu', + hye: 'hy', + ibo: 'ig', + ido: 'io', + iii: 'ii', + iku: 'iu', + ile: 'ie', + ina: 'ia', + ind: 'id', + ipk: 'ik', + isl: 'is', + ita: 'it', + jav: 'jv', + jpn: 'ja', + kal: 'kl', + kan: 'kn', + kas: 'ks', + kat: 'ka', + kau: 'kr', + kaz: 'kk', + khm: 'km', + kik: 'ki', + kin: 'rw', + kir: 'ky', + kom: 'kv', + kon: 'kg', + kor: 'ko', + kua: 'kj', + kur: 'ku', + lao: 'lo', + lat: 'la', + lav: 'lv', + lim: 'li', + lin: 'ln', + lit: 'lt', + ltz: 'lb', + lub: 'lu', + lug: 'lg', + mah: 'mh', + mal: 'ml', + mar: 'mr', + mkd: 'mk', + mlg: 'mg', + mlt: 'mt', + mon: 'mn', + mri: 'mi', + msa: 'ms', + mya: 'my', + nau: 'na', + nav: 'nv', + nbl: 'nr', + nde: 'nd', + ndo: 'ng', + nep: 'ne', + nld: 'nl', + nno: 'nn', + nob: 'nb', + nor: 'no', + nya: 'ny', + oci: 'oc', + oji: 'oj', + ori: 'or', + orm: 'om', + oss: 'os', + pan: 'pa', + pli: 'pi', + pol: 'pl', + por: 'pt', + pus: 'ps', + que: 'qu', + roh: 'rm', + ron: 'ro', + run: 'rn', + rus: 'ru', + sag: 'sg', + san: 'sa', + sin: 'si', + slk: 'sk', + slv: 'sl', + sme: 'se', + smo: 'sm', + sna: 'sn', + snd: 'sd', + som: 'so', + sot: 'st', + spa: 'es', + sqi: 'sq', + srd: 'sc', + srp: 'sr', + ssw: 'ss', + sun: 'su', + swa: 'sw', + swe: 'sv', + tah: 'ty', + tam: 'ta', + tat: 'tt', + tel: 'te', + tgk: 'tg', + tgl: 'tl', + tha: 'th', + tir: 'ti', + ton: 'to', + tsn: 'tn', + tso: 'ts', + tuk: 'tk', + tur: 'tr', + twi: 'tw', + uig: 'ug', + ukr: 'uk', + urd: 'ur', + uzb: 'uz', + ven: 've', + vie: 'vi', + vol: 'vo', + wln: 'wa', + wol: 'wo', + xho: 'xh', + yid: 'yi', + yor: 'yo', + zha: 'za', + zho: 'zh', + zul: 'zu' +} as const + +export type ISO6391 = ValueOf diff --git a/packages/embedder/logMemoryUse.ts b/packages/embedder/logMemoryUse.ts new file mode 100644 index 00000000000..afe3259aee5 --- /dev/null +++ b/packages/embedder/logMemoryUse.ts @@ -0,0 +1,10 @@ +// Not for use in prod, but useful for dev +export const logMemoryUse = () => { + const MB = 2 ** 20 + setInterval(() => { + const memoryUsage = process.memoryUsage() + const {rss} = memoryUsage + const usedMB = Math.floor(rss / MB) + console.log('Memory use:', usedMB, 'MB') + }, 10000) +} diff --git a/packages/embedder/mergeAsyncIterators.ts b/packages/embedder/mergeAsyncIterators.ts new file mode 100644 index 00000000000..e274e0cca6f --- /dev/null +++ b/packages/embedder/mergeAsyncIterators.ts @@ -0,0 +1,96 @@ +import {ParseInt} from '../client/types/generics' + +// can remove PromiseCapability after TS v5.4.2 +type PromiseCapability = { + resolve: (value: T) => void + reject: (reason?: any) => void + promise: Promise +} + +type UnYield = T extends IteratorYieldResult ? U : never +type Result> = UnYield>> + +// Promise.race has a memory leak +// To avoid: https://github.com/tc39/proposal-async-iterator-helpers/issues/15#issuecomment-1937011820 +export function mergeAsyncIterators[] | []>( + iterators: T +): AsyncIterableIterator<{[P in keyof T]: [ParseInt<`${P}`>, Result]}[number]> { + return (async function* () { + type ResultThunk = () => [number, Result] + let count = iterators.length as number + let capability: PromiseCapability | undefined + const queuedResults: ResultThunk[] = [] + const getNext = async (idx: number, iterator: T[number]) => { + try { + const next = await iterator.next() + if (next.done) { + if (--count === 0 && capability !== undefined) { + capability.resolve(null) + } + } else { + resolveResult(() => { + void getNext(idx, iterator) + return [idx, next.value] + }) + } + } catch (error) { + resolveResult(() => { + throw error + }) + } + } + const resolveResult = (resultThunk: ResultThunk) => { + if (capability === undefined) { + queuedResults.push(resultThunk) + } else { + capability.resolve(resultThunk) + } + } + + try { + // Begin all iterators + for (const [idx, iterable] of iterators.entries()) { + void getNext(idx, iterable) + } + + // Delegate to iterables as results complete + while (true) { + while (true) { + const nextQueuedResult = queuedResults.shift() + if (nextQueuedResult === undefined) { + break + } else { + yield nextQueuedResult() + } + } + if (count === 0) { + break + } else { + // Promise.withResolvers() is not yet implemented in node + capability = { + resolve: undefined as any, + reject: undefined as any, + promise: undefined as any + } + capability.promise = new Promise((res, rej) => { + capability!.resolve = res + capability!.reject = rej + }) + const nextResult = await capability.promise + if (nextResult === null) { + break + } else { + capability = undefined + yield nextResult() + } + } + } + } catch (err) { + // Unwind remaining iterators on failure + try { + await Promise.all(iterators.map((iterator) => iterator.return?.())) + } catch {} + throw err + } + })() +} diff --git a/packages/embedder/package.json b/packages/embedder/package.json index 47af8737dac..975309bdb62 100644 --- a/packages/embedder/package.json +++ b/packages/embedder/package.json @@ -1,6 +1,6 @@ { "name": "parabol-embedder", - "version": "7.10.0", + "version": "7.24.1", "description": "A service that computes embedding vectors from Parabol objects", "author": "Jordan Husney ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/embedder#readme", @@ -10,6 +10,10 @@ "url": "git+https://github.com/ParabolInc/parabol.git" }, "scripts": { + "lint": "eslint --fix . --ext .ts,.tsx", + "lint:check": "eslint . --ext .ts,.tsx", + "prettier": "prettier --config ../../.prettierrc --write \"**/*.{ts,tsx}\"", + "prettier:check": "prettier --config ../../.prettierrc --check \"**/*.{ts,tsx}\"", "typecheck": "yarn tsc --noEmit -p tsconfig.json" }, "bugs": { @@ -18,14 +22,17 @@ "devDependencies": { "@babel/cli": "7.18.6", "@babel/core": "7.18.6", + "@types/franc": "^5.0.3", "@types/node": "^16.11.62", "babel-plugin-inline-import": "^3.0.0", + "openapi-fetch": "^0.9.3", "sucrase": "^3.32.0", "ts-node-dev": "^1.0.0-pre.44", - "typescript": "4.9.5" + "typescript": "^5.3.3" }, "dependencies": { "dd-trace": "^4.2.0", + "franc-min": "^5.0.0", "redlock": "^5.0.0-beta.2" } } diff --git a/packages/embedder/processJob.ts b/packages/embedder/processJob.ts new file mode 100644 index 00000000000..8723b75de6a --- /dev/null +++ b/packages/embedder/processJob.ts @@ -0,0 +1,13 @@ +import RootDataLoader from 'parabol-server/dataloader/RootDataLoader' +import {Job} from './EmbeddingsJobQueueStream' +import {processJobEmbed} from './processJobEmbed' + +export const processJob = async (job: Job, dataLoader: RootDataLoader) => { + const {jobType} = job + switch (jobType) { + case 'embed': + return processJobEmbed(job, dataLoader) + default: + throw new Error(`Invalid job type: ${jobType}`) + } +} diff --git a/packages/embedder/processJobEmbed.ts b/packages/embedder/processJobEmbed.ts new file mode 100644 index 00000000000..d7fecf2aab0 --- /dev/null +++ b/packages/embedder/processJobEmbed.ts @@ -0,0 +1,101 @@ +import franc from 'franc-min' +import ms from 'ms' +import RootDataLoader from 'parabol-server/dataloader/RootDataLoader' +import getKysely from 'parabol-server/postgres/getKysely' +import {EmbedJob} from './EmbeddingsJobQueueStream' +import {EmbeddingsTable} from './ai_models/AbstractEmbeddingsModel' +import getModelManager from './ai_models/ModelManager' +import {createEmbeddingTextFrom} from './indexing/createEmbeddingTextFrom' +import {failJob} from './indexing/failJob' +import numberVectorToString from './indexing/numberVectorToString' +import {iso6393To1} from './iso6393To1' + +export const processJobEmbed = async (job: EmbedJob, dataLoader: RootDataLoader) => { + const pg = getKysely() + const {id: jobId, retryCount, jobData} = job + const {embeddingsMetadataId, model} = jobData + const modelManager = getModelManager() + + const metadata = await pg + .selectFrom('EmbeddingsMetadata') + .selectAll() + .where('id', '=', embeddingsMetadataId) + .executeTakeFirst() + + if (!metadata) { + await failJob(jobId, `unable to fetch metadata by EmbeddingsJobQueue.id = ${jobId}`) + return + } + + let {fullText, language} = metadata + try { + if (!fullText) { + fullText = await createEmbeddingTextFrom(metadata, dataLoader) + language = iso6393To1[franc(fullText) as keyof typeof iso6393To1] + await pg + .updateTable('EmbeddingsMetadata') + .set({fullText, language}) + .where('id', '=', embeddingsMetadataId) + .execute() + } + } catch (e) { + // get the trace since the error message may be unobvious + console.trace(e) + await failJob(jobId, `unable to create embedding text: ${e}`) + return + } + + const embeddingModel = modelManager.embeddingModels.get(model) + if (!embeddingModel) { + await failJob(jobId, `embedding model ${model} not available`) + return + } + + // Exit successfully, we don't want to fail the job because the language is not supported + if (!embeddingModel.languages.includes(language!)) return true + + const chunks = await embeddingModel.chunkText(fullText) + if (chunks instanceof Error) { + await failJob( + jobId, + `unable to get tokens: ${chunks.message}`, + retryCount < 10 ? new Date(Date.now() + ms('1m')) : null + ) + return + } + // Cannot use summarization strategy if generation model has same context length as embedding model + // We must split the text & not tokens because BERT tokenizer is not trained for linebreaks e.g. \n\n + const isSuccessful = await Promise.all( + chunks.map(async (chunk, chunkNumber) => { + const embeddingVector = await embeddingModel.getEmbedding(chunk) + if (embeddingVector instanceof Error) { + await failJob( + jobId, + `unable to get embeddings: ${embeddingVector.message}`, + retryCount < 10 ? new Date(Date.now() + ms('1m')) : null + ) + return false + } + await pg + // cast to any because these types won't be available in CI + .insertInto(embeddingModel.tableName as EmbeddingsTable) + .values({ + // TODO is the extra space of a null embedText really worth it?! + embedText: chunks.length > 1 ? chunk : null, + embedding: numberVectorToString(embeddingVector), + embeddingsMetadataId, + chunkNumber: chunks.length > 1 ? chunkNumber : null + }) + .onConflict((oc) => + oc.column('embeddingsMetadataId').doUpdateSet((eb) => ({ + embedText: eb.ref('excluded.embedText'), + embedding: eb.ref('excluded.embedding') + })) + ) + .execute() + return true + }) + ) + // Logger.log(`Embedded ${embeddingsMetadataId} -> ${model}`) + return isSuccessful +} diff --git a/packages/embedder/resetStalledJobs.ts b/packages/embedder/resetStalledJobs.ts new file mode 100644 index 00000000000..592b468faee --- /dev/null +++ b/packages/embedder/resetStalledJobs.ts @@ -0,0 +1,18 @@ +import ms from 'ms' +import getKysely from 'parabol-server/postgres/getKysely' + +export const resetStalledJobs = () => { + setInterval(async () => { + const pg = getKysely() + await pg + .updateTable('EmbeddingsJobQueue') + .set((eb) => ({ + state: 'queued', + startAt: null, + retryCount: eb('retryCount', '+', 1), + stateMessage: 'stalled' + })) + .where('startAt', '<', new Date(Date.now() - ms('5m'))) + .execute() + }, ms('5m')) +} diff --git a/packages/embedder/textEmbeddingsnterface.d.ts b/packages/embedder/textEmbeddingsnterface.d.ts new file mode 100644 index 00000000000..618eea7db91 --- /dev/null +++ b/packages/embedder/textEmbeddingsnterface.d.ts @@ -0,0 +1,877 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +/** OneOf type helpers */ +type Without = {[P in Exclude]?: never} +type XOR = T | U extends object ? (Without & U) | (Without & T) : T | U +type OneOf = T extends [infer Only] + ? Only + : T extends [infer A, infer B, ...infer Rest] + ? OneOf<[XOR, ...Rest]> + : never + +export interface paths { + '/decode': { + /** + * Decode input ids + * @description Decode input ids + */ + post: operations['decode'] + } + '/embed': { + /** + * Get Embeddings. Returns a 424 status code if the model is not an embedding model. + * @description Get Embeddings. Returns a 424 status code if the model is not an embedding model. + */ + post: operations['embed'] + } + '/embed_all': { + /** + * Get all Embeddings without Pooling. + * @description Get all Embeddings without Pooling. + * Returns a 424 status code if the model is not an embedding model. + */ + post: operations['embed_all'] + } + '/embed_sparse': { + /** + * Get Sparse Embeddings. Returns a 424 status code if the model is not an embedding model with SPLADE pooling. + * @description Get Sparse Embeddings. Returns a 424 status code if the model is not an embedding model with SPLADE pooling. + */ + post: operations['embed_sparse'] + } + '/embeddings': { + /** + * OpenAI compatible route. Returns a 424 status code if the model is not an embedding model. + * @description OpenAI compatible route. Returns a 424 status code if the model is not an embedding model. + */ + post: operations['openai_embed'] + } + '/health': { + /** + * Health check method + * @description Health check method + */ + get: operations['health'] + } + '/info': { + /** + * Text Embeddings Inference endpoint info + * @description Text Embeddings Inference endpoint info + */ + get: operations['get_model_info'] + } + '/metrics': { + /** + * Prometheus metrics scrape endpoint + * @description Prometheus metrics scrape endpoint + */ + get: operations['metrics'] + } + '/predict': { + /** + * Get Predictions. Returns a 424 status code if the model is not a Sequence Classification model + * @description Get Predictions. Returns a 424 status code if the model is not a Sequence Classification model + */ + post: operations['predict'] + } + '/rerank': { + /** + * Get Ranks. Returns a 424 status code if the model is not a Sequence Classification model with + * @description Get Ranks. Returns a 424 status code if the model is not a Sequence Classification model with + * a single class. + */ + post: operations['rerank'] + } + '/tokenize': { + /** + * Tokenize inputs + * @description Tokenize inputs + */ + post: operations['tokenize'] + } + '/vertex': { + /** + * Generate embeddings from a Vertex request + * @description Generate embeddings from a Vertex request + */ + post: operations['vertex_compatibility'] + } +} + +export type webhooks = Record + +export interface components { + schemas: { + ClassifierModel: { + /** + * @example { + * "0": "LABEL" + * } + */ + id2label: { + [key: string]: string + } + /** + * @example { + * "LABEL": 0 + * } + */ + label2id: { + [key: string]: number + } + } + DecodeRequest: { + ids: components['schemas']['InputIds'] + /** + * @default true + * @example true + */ + skip_special_tokens?: boolean + } + /** + * @example [ + * "test" + * ] + */ + DecodeResponse: string[] + EmbedAllRequest: { + inputs: components['schemas']['Input'] + /** + * @default false + * @example false + */ + truncate?: boolean + } + /** + * @example [ + * [ + * [ + * 0, + * 1, + * 2 + * ] + * ] + * ] + */ + EmbedAllResponse: number[][][] + EmbedRequest: { + inputs: components['schemas']['Input'] + /** + * @default true + * @example true + */ + normalize?: boolean + /** + * @default false + * @example false + */ + truncate?: boolean + } + /** + * @example [ + * [ + * 0, + * 1, + * 2 + * ] + * ] + */ + EmbedResponse: number[][] + EmbedSparseRequest: { + inputs: components['schemas']['Input'] + /** + * @default false + * @example false + */ + truncate?: boolean + } + EmbedSparseResponse: components['schemas']['SparseValue'][][] + EmbeddingModel: { + /** @example cls */ + pooling: string + } + ErrorResponse: { + error: string + error_type: components['schemas']['ErrorType'] + } + /** @enum {string} */ + ErrorType: 'Unhealthy' | 'Backend' | 'Overloaded' | 'Validation' | 'Tokenizer' + Info: { + /** @example null */ + docker_label?: string | null + /** + * @default null + * @example null + */ + max_batch_requests?: number | null + /** @example 2048 */ + max_batch_tokens: number + /** @example 32 */ + max_client_batch_size: number + /** + * @description Router Parameters + * @example 128 + */ + max_concurrent_requests: number + /** @example 512 */ + max_input_length: number + /** @example float16 */ + model_dtype: string + /** + * @description Model info + * @example thenlper/gte-base + */ + model_id: string + /** @example fca14538aa9956a46526bd1d0d11d69e19b5a101 */ + model_sha?: string | null + model_type: components['schemas']['ModelType'] + /** @example null */ + sha?: string | null + /** @example 4 */ + tokenization_workers: number + /** + * @description Router Info + * @example 0.5.0 + */ + version: string + } + Input: string | string[] + InputIds: number[] | number[][] + ModelType: OneOf< + [ + { + classifier: components['schemas']['ClassifierModel'] + }, + { + embedding: components['schemas']['EmbeddingModel'] + }, + { + reranker: components['schemas']['ClassifierModel'] + } + ] + > + OpenAICompatEmbedding: { + /** + * @example [ + * 0, + * 1, + * 2 + * ] + */ + embedding: number[] + /** @example 0 */ + index: number + /** @example embedding */ + object: string + } + OpenAICompatErrorResponse: { + /** Format: int32 */ + code: number + error_type: components['schemas']['ErrorType'] + message: string + } + OpenAICompatRequest: { + input: components['schemas']['Input'] + /** @example null */ + model?: string | null + /** @example null */ + user?: string | null + } + OpenAICompatResponse: { + data: components['schemas']['OpenAICompatEmbedding'][] + /** @example thenlper/gte-base */ + model: string + /** @example list */ + object: string + usage: components['schemas']['OpenAICompatUsage'] + } + OpenAICompatUsage: { + /** @example 512 */ + prompt_tokens: number + /** @example 512 */ + total_tokens: number + } + /** + * @description Model input. Can be either a single string, a pair of strings or a batch of mixed single and pairs of strings. + * @example What is Deep Learning? + */ + PredictInput: string | string[] | string[][] + PredictRequest: { + inputs: components['schemas']['PredictInput'] + /** + * @default false + * @example false + */ + raw_scores?: boolean + /** + * @default false + * @example false + */ + truncate?: boolean + } + PredictResponse: components['schemas']['Prediction'][] | components['schemas']['Prediction'][][] + Prediction: { + /** @example admiration */ + label: string + /** + * Format: float + * @example 0.5 + */ + score: number + } + Rank: { + /** @example 0 */ + index: number + /** + * Format: float + * @example 1.0 + */ + score: number + /** + * @default null + * @example Deep Learning is ... + */ + text?: string | null + } + RerankRequest: { + /** @example What is Deep Learning? */ + query: string + /** + * @default false + * @example false + */ + raw_scores?: boolean + /** + * @default false + * @example false + */ + return_text?: boolean + /** + * @example [ + * "Deep Learning is ..." + * ] + */ + texts: string[] + /** + * @default false + * @example false + */ + truncate?: boolean + } + RerankResponse: components['schemas']['Rank'][] + SimpleToken: { + /** + * Format: int32 + * @example 0 + */ + id: number + /** @example false */ + special: boolean + /** @example 0 */ + start?: number | null + /** @example 2 */ + stop?: number | null + /** @example test */ + text: string + } + SparseValue: { + index: number + /** Format: float */ + value: number + } + TokenizeRequest: { + /** + * @default true + * @example true + */ + add_special_tokens?: boolean + inputs: components['schemas']['Input'] + } + /** + * @example [ + * [ + * { + * "id": 0, + * "special": false, + * "start": 0, + * "stop": 2, + * "text": "test" + * } + * ] + * ] + */ + TokenizeResponse: components['schemas']['SimpleToken'][][] + VertexInstance: + | (components['schemas']['EmbedRequest'] & { + /** @enum {string} */ + type: 'embed' + }) + | (components['schemas']['EmbedAllRequest'] & { + /** @enum {string} */ + type: 'embed_all' + }) + | (components['schemas']['EmbedSparseRequest'] & { + /** @enum {string} */ + type: 'embed_sparse' + }) + | (components['schemas']['PredictRequest'] & { + /** @enum {string} */ + type: 'predict' + }) + | (components['schemas']['RerankRequest'] & { + /** @enum {string} */ + type: 'rerank' + }) + | (components['schemas']['TokenizeRequest'] & { + /** @enum {string} */ + type: 'tokenize' + }) + VertexRequest: { + instances: components['schemas']['VertexInstance'][] + } + VertexResponse: components['schemas']['VertexResponseInstance'][] + VertexResponseInstance: + | { + result: components['schemas']['EmbedResponse'] + /** @enum {string} */ + type: 'embed' + } + | { + result: components['schemas']['EmbedAllResponse'] + /** @enum {string} */ + type: 'embed_all' + } + | { + result: components['schemas']['EmbedSparseResponse'] + /** @enum {string} */ + type: 'embed_sparse' + } + | { + result: components['schemas']['PredictResponse'] + /** @enum {string} */ + type: 'predict' + } + | { + result: components['schemas']['RerankResponse'] + /** @enum {string} */ + type: 'rerank' + } + | { + result: components['schemas']['TokenizeResponse'] + /** @enum {string} */ + type: 'tokenize' + } + } + responses: never + parameters: never + requestBodies: never + headers: never + pathItems: never +} + +export type $defs = Record + +export type external = Record + +export interface operations { + /** + * Decode input ids + * @description Decode input ids + */ + decode: { + requestBody: { + content: { + 'application/json': components['schemas']['DecodeRequest'] + } + } + responses: { + /** @description Decoded ids */ + 200: { + content: { + 'application/json': components['schemas']['DecodeResponse'] + } + } + /** @description Tokenization error */ + 422: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + /** + * Get Embeddings. Returns a 424 status code if the model is not an embedding model. + * @description Get Embeddings. Returns a 424 status code if the model is not an embedding model. + */ + embed: { + requestBody: { + content: { + 'application/json': components['schemas']['EmbedRequest'] + } + } + responses: { + /** @description Embeddings */ + 200: { + content: { + 'application/json': components['schemas']['EmbedResponse'] + } + } + /** @description Batch size error */ + 413: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Tokenization error */ + 422: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Embedding Error */ + 424: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Model is overloaded */ + 429: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + /** + * Get all Embeddings without Pooling. + * @description Get all Embeddings without Pooling. + * Returns a 424 status code if the model is not an embedding model. + */ + embed_all: { + requestBody: { + content: { + 'application/json': components['schemas']['EmbedAllRequest'] + } + } + responses: { + /** @description Embeddings */ + 200: { + content: { + 'application/json': components['schemas']['EmbedAllResponse'] + } + } + /** @description Batch size error */ + 413: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Tokenization error */ + 422: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Embedding Error */ + 424: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Model is overloaded */ + 429: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + /** + * Get Sparse Embeddings. Returns a 424 status code if the model is not an embedding model with SPLADE pooling. + * @description Get Sparse Embeddings. Returns a 424 status code if the model is not an embedding model with SPLADE pooling. + */ + embed_sparse: { + requestBody: { + content: { + 'application/json': components['schemas']['EmbedSparseRequest'] + } + } + responses: { + /** @description Embeddings */ + 200: { + content: { + 'application/json': components['schemas']['EmbedSparseResponse'] + } + } + /** @description Batch size error */ + 413: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Tokenization error */ + 422: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Embedding Error */ + 424: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Model is overloaded */ + 429: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + /** + * OpenAI compatible route. Returns a 424 status code if the model is not an embedding model. + * @description OpenAI compatible route. Returns a 424 status code if the model is not an embedding model. + */ + openai_embed: { + requestBody: { + content: { + 'application/json': components['schemas']['OpenAICompatRequest'] + } + } + responses: { + /** @description Embeddings */ + 200: { + content: { + 'application/json': components['schemas']['OpenAICompatResponse'] + } + } + /** @description Batch size error */ + 413: { + content: { + 'application/json': components['schemas']['OpenAICompatErrorResponse'] + } + } + /** @description Tokenization error */ + 422: { + content: { + 'application/json': components['schemas']['OpenAICompatErrorResponse'] + } + } + /** @description Embedding Error */ + 424: { + content: { + 'application/json': components['schemas']['OpenAICompatErrorResponse'] + } + } + /** @description Model is overloaded */ + 429: { + content: { + 'application/json': components['schemas']['OpenAICompatErrorResponse'] + } + } + } + } + /** + * Health check method + * @description Health check method + */ + health: { + responses: { + /** @description Everything is working fine */ + 200: { + content: never + } + /** @description Text embeddings Inference is down */ + 503: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + /** + * Text Embeddings Inference endpoint info + * @description Text Embeddings Inference endpoint info + */ + get_model_info: { + responses: { + /** @description Served model info */ + 200: { + content: { + 'application/json': components['schemas']['Info'] + } + } + } + } + /** + * Prometheus metrics scrape endpoint + * @description Prometheus metrics scrape endpoint + */ + metrics: { + responses: { + /** @description Prometheus Metrics */ + 200: { + content: { + 'text/plain': string + } + } + } + } + /** + * Get Predictions. Returns a 424 status code if the model is not a Sequence Classification model + * @description Get Predictions. Returns a 424 status code if the model is not a Sequence Classification model + */ + predict: { + requestBody: { + content: { + 'application/json': components['schemas']['PredictRequest'] + } + } + responses: { + /** @description Predictions */ + 200: { + content: { + 'application/json': components['schemas']['PredictResponse'] + } + } + /** @description Batch size error */ + 413: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Tokenization error */ + 422: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Prediction Error */ + 424: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Model is overloaded */ + 429: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + /** + * Get Ranks. Returns a 424 status code if the model is not a Sequence Classification model with + * @description Get Ranks. Returns a 424 status code if the model is not a Sequence Classification model with + * a single class. + */ + rerank: { + requestBody: { + content: { + 'application/json': components['schemas']['RerankRequest'] + } + } + responses: { + /** @description Ranks */ + 200: { + content: { + 'application/json': components['schemas']['RerankResponse'] + } + } + /** @description Batch size error */ + 413: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Tokenization error */ + 422: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Rerank Error */ + 424: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Model is overloaded */ + 429: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + /** + * Tokenize inputs + * @description Tokenize inputs + */ + tokenize: { + requestBody: { + content: { + 'application/json': components['schemas']['TokenizeRequest'] + } + } + responses: { + /** @description Tokenized ids */ + 200: { + content: { + 'application/json': components['schemas']['TokenizeResponse'] + } + } + /** @description Tokenization error */ + 422: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } + /** + * Generate embeddings from a Vertex request + * @description Generate embeddings from a Vertex request + */ + vertex_compatibility: { + requestBody: { + content: { + 'application/json': components['schemas']['VertexRequest'] + } + } + responses: { + /** @description Results */ + 200: { + content: never + } + /** @description Batch size error */ + 413: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Tokenization error */ + 422: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Error */ + 424: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Model is overloaded */ + 429: { + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } +} diff --git a/packages/embedder/tsconfig.json b/packages/embedder/tsconfig.json index 1d179d08096..b75106d1a8a 100644 --- a/packages/embedder/tsconfig.json +++ b/packages/embedder/tsconfig.json @@ -3,13 +3,10 @@ "compilerOptions": { "baseUrl": "../", "paths": { - // when we import from lib, make goto-definition point to the src "parabol-server/*": ["server/*"], - "parabol-client/*": ["client/*"] + "parabol-client/*": ["client/*"], + "~/*": ["client/*"] }, - "outDir": "lib", - "lib": ["esnext"], - "types": ["node"] - }, - "files": ["../server/types/modules.d.ts"] + "lib": ["esnext"] + } } diff --git a/packages/embedder/types/modules.d.ts b/packages/embedder/types/modules.d.ts new file mode 100644 index 00000000000..276eb185fa4 --- /dev/null +++ b/packages/embedder/types/modules.d.ts @@ -0,0 +1,4 @@ +declare module 'franc-min' { + import f from 'franc' + export = f +} diff --git a/packages/embedder/types/shared.d.ts b/packages/embedder/types/shared.d.ts new file mode 100644 index 00000000000..e94d0d14dea --- /dev/null +++ b/packages/embedder/types/shared.d.ts @@ -0,0 +1 @@ +import '../../server/types/modules' diff --git a/packages/gql-executor/RedisStream.ts b/packages/gql-executor/RedisStream.ts index 739480bd51d..22798509396 100644 --- a/packages/gql-executor/RedisStream.ts +++ b/packages/gql-executor/RedisStream.ts @@ -2,24 +2,25 @@ import RedisInstance from 'parabol-server/utils/RedisInstance' type MessageValue = [prop: string, stringifiedData: string] type Message = [messageId: string, value: MessageValue] -type XReadGroupRes = [streamName: string, messages: Message[]] -export default class RedisStream implements AsyncIterableIterator { +type XReadGroupRes = [streamName: string, messages: [Message, ...Message[]]] +export default class RedisStream implements AsyncIterableIterator { private stream: string private consumerGroup: string // xreadgroup blocks until a response is received, so this needs its own connection - private redis = new RedisInstance('gql_stream') + private redis: RedisInstance private consumer: string constructor(stream: string, consumerGroup: string, consumer: string) { this.stream = stream this.consumerGroup = consumerGroup this.consumer = consumer + this.redis = new RedisInstance(stream) } [Symbol.asyncIterator]() { return this } - async next() { + async next(): Promise> { const response = await this.redis.xreadgroup( 'GROUP', this.consumerGroup, diff --git a/packages/gql-executor/gqlExecutor.ts b/packages/gql-executor/gqlExecutor.ts index 2d4239b22a6..a3ce88d0853 100644 --- a/packages/gql-executor/gqlExecutor.ts +++ b/packages/gql-executor/gqlExecutor.ts @@ -2,9 +2,10 @@ import tracer from 'dd-trace' import {ServerChannel} from 'parabol-client/types/constEnums' import GQLExecutorChannelId from '../client/shared/gqlIds/GQLExecutorChannelId' import SocketServerChannelId from '../client/shared/gqlIds/SocketServerChannelId' -import executeGraphQL, {GQLRequest} from '../server/graphql/executeGraphQL' +import executeGraphQL from '../server/graphql/executeGraphQL' import '../server/initSentry' import '../server/monkeyPatchFetch' +import {GQLRequest} from '../server/types/custom' import RedisInstance from '../server/utils/RedisInstance' import RedisStream from './RedisStream' @@ -16,7 +17,7 @@ tracer.init({ }) tracer.use('ioredis').use('http').use('pg') -const {REDIS_URL, SERVER_ID} = process.env +const {SERVER_ID} = process.env interface PubSubPromiseMessage { jobId: string socketServerId: string @@ -26,7 +27,7 @@ interface PubSubPromiseMessage { const run = async () => { const publisher = new RedisInstance('gql_pub') const subscriber = new RedisInstance('gql_sub') - const executorChannel = GQLExecutorChannelId.join(SERVER_ID) + const executorChannel = GQLExecutorChannelId.join(SERVER_ID!) // on shutdown, remove consumer from the group process.on('SIGTERM', async () => { diff --git a/packages/gql-executor/modules.d.ts b/packages/gql-executor/modules.d.ts new file mode 100644 index 00000000000..8a2be20ba0c --- /dev/null +++ b/packages/gql-executor/modules.d.ts @@ -0,0 +1,2 @@ +import '../server/types/modules' +import '../server/types/webpackEnv' diff --git a/packages/gql-executor/package.json b/packages/gql-executor/package.json index bed8c3473eb..3797dee3e75 100644 --- a/packages/gql-executor/package.json +++ b/packages/gql-executor/package.json @@ -1,6 +1,6 @@ { "name": "gql-executor", - "version": "7.23.1", + "version": "7.24.1", "description": "A Stateless GraphQL Executor", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/gqlExecutor#readme", @@ -27,8 +27,8 @@ }, "dependencies": { "dd-trace": "^4.2.0", - "parabol-client": "7.23.1", - "parabol-server": "7.23.1", + "parabol-client": "7.24.1", + "parabol-server": "7.24.1", "undici": "^5.26.2" } } diff --git a/packages/gql-executor/tsconfig.json b/packages/gql-executor/tsconfig.json index 1d179d08096..5f1fdbf2d75 100644 --- a/packages/gql-executor/tsconfig.json +++ b/packages/gql-executor/tsconfig.json @@ -1,15 +1,14 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "esModuleInterop": true, "baseUrl": "../", "paths": { // when we import from lib, make goto-definition point to the src "parabol-server/*": ["server/*"], - "parabol-client/*": ["client/*"] + "parabol-client/*": ["client/*"], + "~/*": ["client/*"] }, - "outDir": "lib", - "lib": ["esnext"], - "types": ["node"] - }, - "files": ["../server/types/modules.d.ts"] + "lib": ["esnext"] + } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 2cbc84391c1..84ae4bb4e29 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -2,7 +2,7 @@ "name": "integration-tests", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.23.1", + "version": "7.24.1", "description": "", "main": "index.js", "scripts": { @@ -11,8 +11,6 @@ }, "devDependencies": { "@playwright/test": "^1.34.3", - "eslint": "^8.8.0", - "eslint-config-prettier": "^8.5.0", "lint-staged": "^12.3.3", "ts-app-env": "^1.4.2", "typescript": "^5.3.3" diff --git a/packages/server/__tests__/autoJoin.test.ts b/packages/server/__tests__/autoJoin.test.ts index 85bc9e3e222..3fe4564e6eb 100644 --- a/packages/server/__tests__/autoJoin.test.ts +++ b/packages/server/__tests__/autoJoin.test.ts @@ -1,11 +1,11 @@ import faker from 'faker' -import {getUserTeams, sendPublic, sendIntranet, signUp, signUpWithEmail} from './common' import getRethink from '../database/rethinkDriver' import createEmailVerification from '../email/createEmailVerification' +import {getUserTeams, sendIntranet, sendPublic} from './common' const signUpVerified = async (email: string) => { const password = faker.internet.password() - const signUp = await sendPublic({ + await sendPublic({ query: ` mutation SignUpWithPassword($email: ID!, $password: String!) { signUpWithPassword(email: $email, password: $password, params: "") { diff --git a/packages/server/__tests__/common.ts b/packages/server/__tests__/common.ts index f9ca34ab293..93c134b229b 100644 --- a/packages/server/__tests__/common.ts +++ b/packages/server/__tests__/common.ts @@ -199,5 +199,5 @@ export const getUserTeams = async (userId: string) => { } } }) - return user.data.user.teams + return user.data.user.teams as [{id: string}, ...{id: string}[]] } diff --git a/packages/server/__tests__/disableAnonymity.test.ts b/packages/server/__tests__/disableAnonymity.test.ts index 7143981aa36..1887c3357a0 100644 --- a/packages/server/__tests__/disableAnonymity.test.ts +++ b/packages/server/__tests__/disableAnonymity.test.ts @@ -89,8 +89,12 @@ const startRetro = async (teamId: string, authToken: string) => { }, authToken }) - - const meeting = startRetroQuery.data.startRetrospective.meeting + const meeting = startRetroQuery.data.startRetrospective.meeting as { + id: string + phases: { + reflectPrompts: [{id: string}] + }[] + } return meeting } @@ -134,7 +138,7 @@ test('By default all reflections are anonymous', async () => { expect(meetingSettings.disableAnonymity).toEqual(false) const meeting = await startRetro(teamId, authToken) - const reflectPrompts = meeting.phases.find(({reflectPrompts}) => !!reflectPrompts).reflectPrompts + const reflectPrompts = meeting.phases.find(({reflectPrompts}) => !!reflectPrompts)!.reflectPrompts const reflection = await addReflection(meeting.id, reflectPrompts[0].id, authToken) expect(reflection).toEqual({ @@ -153,7 +157,7 @@ test('Creator is visible when disableAnonymity is set', async () => { expect(updatedMeetingSettings.disableAnonymity).toEqual(true) const meeting = await startRetro(teamId, authToken) - const reflectPrompts = meeting.phases.find(({reflectPrompts}) => !!reflectPrompts).reflectPrompts + const reflectPrompts = meeting.phases.find(({reflectPrompts}) => !!reflectPrompts)!.reflectPrompts const reflection = await addReflection(meeting.id, reflectPrompts[0].id, authToken) expect(reflection).toEqual({ @@ -172,7 +176,7 @@ test('Super user can always read creatorId of a reflection', async () => { expect(meetingSettings.disableAnonymity).toEqual(false) const meeting = await startRetro(teamId, authToken) - const reflectPrompts = meeting.phases.find(({reflectPrompts}) => !!reflectPrompts).reflectPrompts + const reflectPrompts = meeting.phases.find(({reflectPrompts}) => !!reflectPrompts)!.reflectPrompts await addReflection(meeting.id, reflectPrompts[0].id, authToken) diff --git a/packages/server/__tests__/loginSAML.test.ts b/packages/server/__tests__/loginSAML.test.ts index 426f9606140..312bb3154e5 100644 --- a/packages/server/__tests__/loginSAML.test.ts +++ b/packages/server/__tests__/loginSAML.test.ts @@ -10,30 +10,6 @@ test.skip('SAML', async () => { const orgId = `${samlName}-orgId` const domain = 'example.com' - const _metadata = ` - - - - - - - MIICUDCCAbmgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBFMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExEzARBgNVBAoMCkV4YW1wbGUgQ28xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTIxMDkxMDA3NTkzMFoXDTI2MDkwOTA3NTkzMFowRTELMAkGA1UEBhMCdXMxCzAJBgNVBAgMAkNBMRMwEQYDVQQKDApFeGFtcGxlIENvMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEArg9DZwR9v7Vok1IW+hIpYin9llPBh1MV5CxjfK596EwuadyQuko3jGv8qDlx4tG6JiGTjQfCuzJVAhYi2OKuKBqyJewKoen1uF0dRyws9n6zZl0GsVJkObdrNo5P6eib3VOsXPJ10RjxWsWx5WRur2dYdkOJFxC6zN1IbXSXYYMCAwEAAaNQME4wHQYDVR0OBBYEFKr/1y4R+kamPz623HnHM7tz6C4XMB8GA1UdIwQYMBaAFKr/1y4R+kamPz623HnHM7tz6C4XMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQENBQADgYEALKBl6QPk9HMB5V+GYu50XNFmzyuuXt3zAKMSYcyhxVSBCe6SKw1iqvvPza4rGp7DpeJI/8R3qBTuZqfl0rX624wvHGc4N9WubMLPejAn7dMu3oGfm9KUX+Um1RG0U6zsi9t3X90rroea/5SQvw/uAWUxS59U2r8massI/WFJKh8= - - - - - - - MIICUDCCAbmgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBFMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExEzARBgNVBAoMCkV4YW1wbGUgQ28xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTIxMDkxMDA3NTkzMFoXDTI2MDkwOTA3NTkzMFowRTELMAkGA1UEBhMCdXMxCzAJBgNVBAgMAkNBMRMwEQYDVQQKDApFeGFtcGxlIENvMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEArg9DZwR9v7Vok1IW+hIpYin9llPBh1MV5CxjfK596EwuadyQuko3jGv8qDlx4tG6JiGTjQfCuzJVAhYi2OKuKBqyJewKoen1uF0dRyws9n6zZl0GsVJkObdrNo5P6eib3VOsXPJ10RjxWsWx5WRur2dYdkOJFxC6zN1IbXSXYYMCAwEAAaNQME4wHQYDVR0OBBYEFKr/1y4R+kamPz623HnHM7tz6C4XMB8GA1UdIwQYMBaAFKr/1y4R+kamPz623HnHM7tz6C4XMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQENBQADgYEALKBl6QPk9HMB5V+GYu50XNFmzyuuXt3zAKMSYcyhxVSBCe6SKw1iqvvPza4rGp7DpeJI/8R3qBTuZqfl0rX624wvHGc4N9WubMLPejAn7dMu3oGfm9KUX+Um1RG0U6zsi9t3X90rroea/5SQvw/uAWUxS59U2r8massI/WFJKh8= - - - - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - - - ` - const verifyDomain = await sendIntranet({ query: ` mutation VerifyDomain($slug: ID!, $addDomains: [ID!], $orgId: ID!) { diff --git a/packages/server/__tests__/processRecurrence.test.ts b/packages/server/__tests__/processRecurrence.test.ts index f6e6c685715..fab6970bc9a 100644 --- a/packages/server/__tests__/processRecurrence.test.ts +++ b/packages/server/__tests__/processRecurrence.test.ts @@ -1,14 +1,13 @@ import ms from 'ms' import {RRule} from 'rrule' import getRethink from '../database/rethinkDriver' -import MeetingTeamPrompt from '../database/types/MeetingTeamPrompt' -import TeamPromptResponsesPhase from '../database/types/TeamPromptResponsesPhase' import MeetingRetrospective from '../database/types/MeetingRetrospective' +import MeetingTeamPrompt from '../database/types/MeetingTeamPrompt' import ReflectPhase from '../database/types/ReflectPhase' +import TeamPromptResponsesPhase from '../database/types/TeamPromptResponsesPhase' import generateUID from '../generateUID' import {insertMeetingSeries as insertMeetingSeriesQuery} from '../postgres/queries/insertMeetingSeries' import {getUserTeams, sendIntranet, signUp} from './common' -import createNewMeetingPhases from '../graphql/mutations/helpers/createNewMeetingPhases' const PROCESS_RECURRENCE = ` mutation { @@ -292,7 +291,11 @@ test('Should end the current retro meeting and start a new meeting', async () => facilitatorUserId: userId, scheduledEndTime: new Date(Date.now() - ms('5m')), meetingSeriesId, - templateId: 'startStopContinueTemplate' + templateId: 'startStopContinueTemplate', + disableAnonymity: false, + totalVotes: 5, + name: '', + maxVotesPerGroup: 5 }) // The last meeting in the series was created just over 24h ago, so the next one should start diff --git a/packages/server/__tests__/startRetrospective.test.ts b/packages/server/__tests__/startRetrospective.test.ts index 22ff21a7f12..17deae321e5 100644 --- a/packages/server/__tests__/startRetrospective.test.ts +++ b/packages/server/__tests__/startRetrospective.test.ts @@ -1,14 +1,8 @@ -import ms from 'ms' -import {RRule} from 'rrule' import getRethink from '../database/rethinkDriver' -import MeetingTeamPrompt from '../database/types/MeetingTeamPrompt' -import TeamPromptResponsesPhase from '../database/types/TeamPromptResponsesPhase' -import generateUID from '../generateUID' -import {insertMeetingSeries as insertMeetingSeriesQuery} from '../postgres/queries/insertMeetingSeries' -import {getUserTeams, sendIntranet, sendPublic, signUp} from './common' +import {getUserTeams, sendPublic, signUp} from './common' test('Retro is named Retro #1 by default', async () => { - const r = await getRethink() + await getRethink() const {userId, authToken} = await signUp() const {id: teamId} = (await getUserTeams(userId))[0] @@ -48,7 +42,7 @@ test('Retro is named Retro #1 by default', async () => { }) test('Recurring retro is named like RetroSeries Jan 1', async () => { - const r = await getRethink() + await getRethink() const {userId, authToken} = await signUp() const {id: teamId} = (await getUserTeams(userId))[0] @@ -81,7 +75,11 @@ test('Recurring retro is named like RetroSeries Jan 1', async () => { authToken }) - const formattedDate = now.toLocaleDateString('en-US', {month: 'short', day: 'numeric'}, 'UTC') + const formattedDate = now.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + timeZone: 'UTC' + }) expect(newRetro).toMatchObject({ data: { startRetrospective: { diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index 5f7311c0580..370482933b2 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -4,7 +4,7 @@ import SlackAuth from '../database/types/SlackAuth' import SlackNotification from '../database/types/SlackNotification' import TeamInvitation from '../database/types/TeamInvitation' import TeamMember from '../database/types/TeamMember' -import {ScheduledJobUnion} from '../graphql/private/mutations/runScheduledJobs' +import {ScheduledJobUnion} from '../types/custom' import {AnyMeeting, AnyMeetingSettings, AnyMeetingTeamMember} from '../postgres/types/Meeting' import getRethinkConfig from './getRethinkConfig' import {R} from './stricterR' diff --git a/packages/server/dataloader/__tests__/isCompanyDomain.test.ts b/packages/server/dataloader/__tests__/isCompanyDomain.test.ts index 172958a60d6..d364d2a7d61 100644 --- a/packages/server/dataloader/__tests__/isCompanyDomain.test.ts +++ b/packages/server/dataloader/__tests__/isCompanyDomain.test.ts @@ -1,7 +1,5 @@ -import faker from 'faker' import '../../../../scripts/webpack/utils/dotenv' import getDataLoader from '../../graphql/getDataLoader' -import getPg from '../../postgres/getPg' const dataloader = getDataLoader() diff --git a/packages/server/dataloader/__tests__/isOrgVerified.test.ts b/packages/server/dataloader/__tests__/isOrgVerified.test.ts index b12897d4b3a..58247c515ab 100644 --- a/packages/server/dataloader/__tests__/isOrgVerified.test.ts +++ b/packages/server/dataloader/__tests__/isOrgVerified.test.ts @@ -1,18 +1,22 @@ /* eslint-env jest */ -import {MasterPool, r} from 'rethinkdb-ts' -import getRedis from '../../utils/getRedis' +import {r} from 'rethinkdb-ts' import getRethinkConfig from '../../database/getRethinkConfig' import getRethink from '../../database/rethinkDriver' -import isUserVerified from '../../utils/isUserVerified' +import OrganizationUser from '../../database/types/OrganizationUser' import generateUID from '../../generateUID' +import {DataLoaderWorker} from '../../graphql/graphql' +import getRedis from '../../utils/getRedis' +import isUserVerified from '../../utils/isUserVerified' +import RootDataLoader from '../RootDataLoader' import {isOrgVerified} from '../customLoaderMakers' jest.mock('../../database/rethinkDriver') jest.mock('../../utils/isUserVerified') -getRethink.mockImplementation(() => { - return r +jest.mocked(getRethink).mockImplementation(() => { + return r as any }) -isUserVerified.mockImplementation(() => { + +jest.mocked(isUserVerified).mockImplementation(() => { return true }) @@ -24,7 +28,7 @@ const testConfig = { db: TEST_DB } -const createTables = async (...tables: string) => { +const createTables = async (...tables: string[]) => { for (const tableName of tables) { const structure = await r .db('rethinkdb') @@ -40,9 +44,10 @@ const createTables = async (...tables: string) => { } } -type TestOrganizationUser = Pick< - OrganizationUser, - 'inactive' | 'joinedAt' | 'removedAt' | 'role' | 'userId' +type TestOrganizationUser = Partial< + Pick & { + domain: string + } > const userLoader = { @@ -61,9 +66,9 @@ const dataLoader = { users: userLoader, isCompanyDomain: isCompanyDomainLoader } - return loaders[loader] + return loaders[loader as keyof typeof loaders] }) -} +} as any as DataLoaderWorker const addOrg = async ( activeDomain: string | null, @@ -101,10 +106,10 @@ const addOrg = async ( return orgId } -const isOrgVerifiedLoader = isOrgVerified(dataLoader) +const isOrgVerifiedLoader = isOrgVerified(dataLoader as any as RootDataLoader) beforeAll(async () => { - const conn = await r.connectPool(testConfig) + await r.connectPool(testConfig) try { await r.dbDrop(TEST_DB).run() } catch (e) { @@ -121,7 +126,7 @@ afterEach(async () => { }) afterAll(async () => { - await r.getPoolMaster().drain() + await r.getPoolMaster()?.drain() getRedis().quit() }) @@ -203,12 +208,12 @@ test('Empty org does not throw', async () => { }) test('Orgs with verified emails from different domains do not qualify', async () => { - const org1 = await addOrg('parabol.co', [ + await addOrg('parabol.co', [ { joinedAt: new Date('2023-09-06'), userId: 'founder1', domain: 'not-parabol.co' - } + } as any ]) const isVerified = await isOrgVerifiedLoader.load('parabol.co') diff --git a/packages/server/dataloader/__tests__/usersCustomRedisQueries.test.ts b/packages/server/dataloader/__tests__/usersCustomRedisQueries.test.ts index 0aa1d0a3162..50fcc7fa77f 100644 --- a/packages/server/dataloader/__tests__/usersCustomRedisQueries.test.ts +++ b/packages/server/dataloader/__tests__/usersCustomRedisQueries.test.ts @@ -1,6 +1,7 @@ import faker from 'faker' import '../../../../scripts/webpack/utils/dotenv' import getDataLoader from '../../graphql/getDataLoader' +import isValid from '../../graphql/isValid' import getPg from '../../postgres/getPg' afterAll(async () => { @@ -17,16 +18,19 @@ test('Result is mapped to correct id', async () => { const dataloader = getDataLoader() const expectedUsers = faker.helpers.shuffle( - (await pg.query('SELECT "id", "email" FROM "User" LIMIT 100')).rows + (await pg.query('SELECT "id", "email" FROM "User" LIMIT 100')).rows as { + id: string + email: string + }[] ) const userIds = expectedUsers.map(({id}) => id) - const actualUsers = (await (dataloader.get('users') as any).loadMany(userIds)).map( - ({id, email}) => ({ + const actualUsers = (await dataloader.get('users').loadMany(userIds)) + .filter(isValid) + .map(({id, email}) => ({ id, email - }) - ) + })) console.log('Ran with #users:', actualUsers.length) diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index ac7e1ea6ec7..f10dfd1f257 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -5,11 +5,11 @@ import getRethink, {RethinkSchema} from '../database/rethinkDriver' import {RDatum} from '../database/stricterR' import MeetingSettingsTeamPrompt from '../database/types/MeetingSettingsTeamPrompt' import MeetingTemplate from '../database/types/MeetingTemplate' +import Organization from '../database/types/Organization' import OrganizationUser from '../database/types/OrganizationUser' import {Reactable, ReactableEnum} from '../database/types/Reactable' import Task, {TaskStatusEnum} from '../database/types/Task' import isValid from '../graphql/isValid' -import {Organization} from '../graphql/public/resolverTypes' import {SAMLSource} from '../graphql/public/types/SAML' import getKysely from '../postgres/getKysely' import {TeamMeetingTemplate} from '../postgres/pg.d' diff --git a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts index 7c19ba2d254..70a038b712c 100644 --- a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts @@ -192,6 +192,19 @@ export const retroReflectionsByMeetingId = new RethinkForeignKeyLoaderMaker( } ) +export const retroReflectionsByGroupId = new RethinkForeignKeyLoaderMaker( + 'retroReflections', + 'reflectionGroupId', + async (reflectionGroupIds) => { + const r = await getRethink() + return r + .table('RetroReflection') + .getAll(r.args(reflectionGroupIds), {index: 'reflectionGroupId'}) + .filter({isActive: true}) + .run() + } +) + export const templateDimensionsByTemplateId = new RethinkForeignKeyLoaderMaker( 'templateDimensions', 'templateId', diff --git a/packages/server/email/inlineImages.ts b/packages/server/email/inlineImages.ts index d02c7bf6448..176a10d5798 100644 --- a/packages/server/email/inlineImages.ts +++ b/packages/server/email/inlineImages.ts @@ -20,7 +20,7 @@ const getFile = async (pathname: string) => { try { const res = await fetch(pathname) if (res.status !== 200) return null - data = await res.buffer() + data = await (res as any).buffer() } catch (e) { return null } @@ -42,7 +42,7 @@ const inlineImages = async (html: string) => { $('body') .find('img') .each((_i, img) => { - const pathname = $(img).attr('src') + const pathname = $(img).attr('src') as keyof typeof cidDict if (!pathname) return cidDict[pathname] = cidDict[pathname] || generateUID() + path.extname(pathname) $(img).attr('src', `cid:${cidDict[pathname]}`) @@ -51,7 +51,7 @@ const inlineImages = async (html: string) => { const files = await Promise.all(uniquePathnames.map(getFile)) const options = files.map((data, idx) => { if (!data) return null - const pathname = uniquePathnames[idx] + const pathname = uniquePathnames[idx] as keyof typeof cidDict const filename = cidDict[pathname] return {data, filename} }) diff --git a/packages/server/generateUID.ts b/packages/server/generateUID.ts index 15d3a616e70..822843fa52d 100644 --- a/packages/server/generateUID.ts +++ b/packages/server/generateUID.ts @@ -8,7 +8,7 @@ const SEQ_BIT_LEN = 12 const TS_OFFSET = BigInt(MACHINE_ID_BIT_LEN + SEQ_BIT_LEN) const MID_OFFSET = BigInt(SEQ_BIT_LEN) const BIG_ZERO = BigInt(0) -const MAX_SEQ = 2 ** SEQ_BIT_LEN - 1 +export const MAX_SEQ = 2 ** SEQ_BIT_LEN - 1 // if MID overflows, we will generate duplicate ids, throw instead if (MID < 0 || MID > 2 ** MACHINE_ID_BIT_LEN - 1) { diff --git a/packages/server/graphql/executeGraphQL.ts b/packages/server/graphql/executeGraphQL.ts index 6b6ad34ea72..8fea40bc2a5 100644 --- a/packages/server/graphql/executeGraphQL.ts +++ b/packages/server/graphql/executeGraphQL.ts @@ -7,30 +7,13 @@ import tracer from 'dd-trace' import {graphql} from 'graphql' import {FormattedExecutionResult} from 'graphql/execution/execute' -import AuthToken from '../database/types/AuthToken' +import type {GQLRequest} from '../types/custom' import CompiledQueryCache from './CompiledQueryCache' import getDataLoader from './getDataLoader' import getRateLimiter from './getRateLimiter' import privateSchema from './private/rootSchema' import publicSchema from './public/rootSchema' -export interface GQLRequest { - authToken: AuthToken - ip?: string - socketId?: string - variables?: {[key: string]: any} - docId?: string - query?: string - rootValue?: {[key: string]: any} - dataLoaderId?: string - // true if the query is on the private schema - isPrivate?: boolean - // true if the query is ad-hoc (e.g. GraphiQL, CLI) - isAdHoc?: boolean - // Datadog opentracing span of the calling server - carrier?: any -} - const queryCache = new CompiledQueryCache() const executeGraphQL = async (req: GQLRequest) => { diff --git a/packages/server/graphql/mutations/helpers/publishToEmbedder.ts b/packages/server/graphql/mutations/helpers/publishToEmbedder.ts new file mode 100644 index 00000000000..c8a735f4ac9 --- /dev/null +++ b/packages/server/graphql/mutations/helpers/publishToEmbedder.ts @@ -0,0 +1,14 @@ +import type {MessageToEmbedder} from 'embedder/custom' +import getRedis from '../../../utils/getRedis' + +export const publishToEmbedder = (message: MessageToEmbedder) => { + return getRedis().xadd( + 'embedMetadataStream', + 'MAXLEN', + '~', + 1000, + '*', + 'msg', + JSON.stringify(message) + ) +} diff --git a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts index cd116c65ea8..f5a69be4f22 100644 --- a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts @@ -1,35 +1,36 @@ +import {RawDraftContentState} from 'draft-js' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {DISCUSS, PARABOL_AI_USER_ID} from 'parabol-client/utils/constants' import getMeetingPhase from 'parabol-client/utils/getMeetingPhase' import findStageById from 'parabol-client/utils/meetings/findStageById' -import {RawDraftContentState} from 'draft-js' import {checkTeamsLimit} from '../../../billing/helpers/teamLimitsCheck' import getRethink from '../../../database/rethinkDriver' import {RDatum} from '../../../database/stricterR' import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import NotificationMentioned from '../../../database/types/NotificationMentioned' import TimelineEventRetroComplete from '../../../database/types/TimelineEventRetroComplete' import getKysely from '../../../postgres/getKysely' import removeSuggestedAction from '../../../safeMutations/removeSuggestedAction' +import {Logger} from '../../../utils/Logger' +import RecallAIServerManager from '../../../utils/RecallAIServerManager' import {analytics} from '../../../utils/analytics/analytics' import {getUserId} from '../../../utils/authorization' import getPhase from '../../../utils/getPhase' import publish from '../../../utils/publish' -import publishNotification from '../../public/mutations/helpers/publishNotification' -import RecallAIServerManager from '../../../utils/RecallAIServerManager' import sendToSentry from '../../../utils/sendToSentry' import standardError from '../../../utils/standardError' import {InternalContext} from '../../graphql' -import updateTeamInsights from './updateTeamInsights' +import publishNotification from '../../public/mutations/helpers/publishNotification' import sendNewMeetingSummary from './endMeeting/sendNewMeetingSummary' +import gatherInsights from './gatherInsights' import generateWholeMeetingSentimentScore from './generateWholeMeetingSentimentScore' import generateWholeMeetingSummary from './generateWholeMeetingSummary' import handleCompletedStage from './handleCompletedStage' import {IntegrationNotifier} from './notifications/IntegrationNotifier' +import {publishToEmbedder} from './publishToEmbedder' import removeEmptyTasks from './removeEmptyTasks' import updateQualAIMeetingsCount from './updateQualAIMeetingsCount' -import gatherInsights from './gatherInsights' -import NotificationMentioned from '../../../database/types/NotificationMentioned' -import {Logger} from '../../../utils/Logger' +import updateTeamInsights from './updateTeamInsights' const getTranscription = async (recallBotId?: string | null) => { if (!recallBotId) return @@ -370,6 +371,7 @@ const safeEndRetrospective = async ({ removedTaskIds, timelineEventId } + publishToEmbedder({objectTypes: ['retrospectiveDiscussionTopic'], meetingId}) publish(SubscriptionChannel.TEAM, teamId, 'EndRetrospectiveSuccess', data, subOptions) return data diff --git a/packages/server/graphql/private/mutations/runScheduledJobs.ts b/packages/server/graphql/private/mutations/runScheduledJobs.ts index aebb1e5eafe..5317aff0147 100644 --- a/packages/server/graphql/private/mutations/runScheduledJobs.ts +++ b/packages/server/graphql/private/mutations/runScheduledJobs.ts @@ -40,8 +40,6 @@ const processMeetingStageTimeLimits = async ( }) } -export type ScheduledJobUnion = ScheduledJobMeetingStageTimeLimit | ScheduledTeamLimitsJob - const processJob = async (job: Selectable, dataLoader: DataLoaderWorker) => { const pg = getKysely() const res = await pg.deleteFrom('ScheduledJob').where('id', '=', job.id).executeTakeFirst() diff --git a/packages/server/graphql/public/rootSchema.ts b/packages/server/graphql/public/rootSchema.ts index b6f1d26fad2..e65b1459189 100644 --- a/packages/server/graphql/public/rootSchema.ts +++ b/packages/server/graphql/public/rootSchema.ts @@ -99,5 +99,4 @@ const addRequestors = (schema: GraphQLSchema) => { const rootSchema = addRequestors(resolveTypesForMutationPayloads(parabolWithNestedResolversSchema)) -export type RootSchema = typeof rootSchema export default rootSchema diff --git a/packages/server/integrations/gitlab/GitLabServerManager.ts b/packages/server/integrations/gitlab/GitLabServerManager.ts index 2ffcfa826bf..dd44a3a5525 100644 --- a/packages/server/integrations/gitlab/GitLabServerManager.ts +++ b/packages/server/integrations/gitlab/GitLabServerManager.ts @@ -9,8 +9,8 @@ import getIssue from '../../graphql/nestedSchema/GitLab/queries/getIssue.graphql import getProfile from '../../graphql/nestedSchema/GitLab/queries/getProfile.graphql' import getProjectIssues from '../../graphql/nestedSchema/GitLab/queries/getProjectIssues.graphql' import getProjects from '../../graphql/nestedSchema/GitLab/queries/getProjects.graphql' -import {RootSchema} from '../../graphql/public/rootSchema' import {IGetTeamMemberIntegrationAuthQueryResult} from '../../postgres/queries/generated/getTeamMemberIntegrationAuthQuery' +import {RootSchema} from '../../types/custom' import { CreateIssueMutation, CreateNoteMutation, diff --git a/packages/server/package.json b/packages/server/package.json index 68cfcfbb846..d619f4b0860 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.23.1", + "version": "7.24.1", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -32,6 +32,7 @@ "@types/bcryptjs": "^2.4.2", "@types/cheerio": "^0.22.30", "@types/dotenv": "^6.1.1", + "@types/faker": "^5.5.9", "@types/graphql": "^14.5.0", "@types/html-minifier-terser": "5.1.2", "@types/jest": "^29.5.1", @@ -49,8 +50,6 @@ "clean-webpack-plugin": "^3.0.0", "copy-webpack-plugin": "7.0.0", "css-loader": "5.0.1", - "eslint": "^8.2.0", - "eslint-config-prettier": "^8.5.0", "faker": "^5.5.3", "file-loader": "6.2.0", "graphql-typed": "^0.4.1", @@ -124,7 +123,7 @@ "oauth-1.0a": "^2.2.6", "openai": "^4.24.1", "oy-vey": "^0.12.1", - "parabol-client": "7.23.1", + "parabol-client": "7.24.1", "pg": "^8.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/packages/server/postgres/migrations/1703031300000_addEmbeddingTables.ts b/packages/server/postgres/migrations/1703031300000_addEmbeddingTables.ts index e5f6f813877..33718ee75a1 100644 --- a/packages/server/postgres/migrations/1703031300000_addEmbeddingTables.ts +++ b/packages/server/postgres/migrations/1703031300000_addEmbeddingTables.ts @@ -1,7 +1,7 @@ import {Client} from 'pg' -import getPgConfig from '../getPgConfig' import {r} from 'rethinkdb-ts' import connectRethinkDB from '../../database/connectRethinkDB' +import getPgConfig from '../getPgConfig' export async function up() { const client = new Client(getPgConfig()) @@ -78,8 +78,8 @@ export async function down() { EXECUTE 'DROP TABLE IF EXISTS "EmbeddingsJobQueue"'; EXECUTE 'DROP TABLE IF EXISTS "EmbeddingsMetadata"'; - EXECUTE 'DROP TYPE IF EXISTS "EmbeddingsStateEnum"'; - EXECUTE 'DROP TYPE IF EXISTS "EmbeddingsObjectTypeEnum"'; + EXECUTE 'DROP TYPE IF EXISTS "EmbeddingsStateEnum" CASCADE'; + EXECUTE 'DROP TYPE IF EXISTS "EmbeddingsObjectTypeEnum" CASCADE'; END $$; `) await client.end() diff --git a/packages/server/postgres/migrations/1709934935000_embeddingsMetadataId.ts b/packages/server/postgres/migrations/1709934935000_embeddingsMetadataId.ts new file mode 100644 index 00000000000..185b1e0881b --- /dev/null +++ b/packages/server/postgres/migrations/1709934935000_embeddingsMetadataId.ts @@ -0,0 +1,62 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + // wipe data to ensure the non-null constraints succeed + await client.query(` + CREATE TYPE "ISO6391Enum" AS ENUM ('aa', 'ab', 'af', 'ak', 'am', 'ar', 'an', 'as', 'av', 'ae', 'ay', 'az', 'ba', 'bm', 'be', 'bn', 'bi', 'bo', 'bs', 'br', 'bg', 'ca', 'cs', 'ch', 'ce', 'cu', 'cv', 'kw', 'co', 'cr', 'cy', 'da', 'de', 'dv', 'dz', 'el', 'en', 'eo', 'et', 'eu', 'ee', 'fo', 'fa', 'fj', 'fi', 'fr', 'fy', 'ff', 'gd', 'ga', 'gl', 'gv', 'gn', 'gu', 'ht', 'ha', 'sh', 'he', 'hz', 'hi', 'ho', 'hr', 'hu', 'hy', 'ig', 'io', 'ii', 'iu', 'ie', 'ia', 'id', 'ik', 'is', 'it', 'jv', 'ja', 'kl', 'kn', 'ks', 'ka', 'kr', 'kk', 'km', 'ki', 'rw', 'ky', 'kv', 'kg', 'ko', 'kj', 'ku', 'lo', 'la', 'lv', 'li', 'ln', 'lt', 'lb', 'lu', 'lg', 'mh', 'ml', 'mr', 'mk', 'mg', 'mt', 'mn', 'mi', 'ms', 'my', 'na', 'nv', 'nr', 'nd', 'ng', 'ne', 'nl', 'nn', 'nb', 'no', 'ny', 'oc', 'oj', 'or', 'om', 'os', 'pa', 'pi', 'pl', 'pt', 'ps', 'qu', 'rm', 'ro', 'rn', 'ru', 'sg', 'sa', 'si', 'sk', 'sl', 'se', 'sm', 'sn', 'sd', 'so', 'st', 'es', 'sq', 'sc', 'sr', 'ss', 'su', 'sw', 'sv', 'ty', 'ta', 'tt', 'te', 'tg', 'tl', 'th', 'ti', 'to', 'tn', 'ts', 'tk', 'tr', 'tw', 'ug', 'uk', 'ur', 'uz', 've', 'vi', 'vo', 'wa', 'wo', 'xh', 'yi', 'yo', 'za', 'zh', 'zu'); + DELETE FROM "EmbeddingsMetadata"; + DELETE FROM "EmbeddingsJobQueue"; + CREATE INDEX IF NOT EXISTS "idx_Discussion_createdAt" ON "Discussion"("createdAt"); + ALTER TYPE "EmbeddingsStateEnum" RENAME VALUE 'embedding' TO 'running'; + ALTER TYPE "EmbeddingsStateEnum" RENAME TO "EmbeddingsJobStateEnum"; + ALTER TABLE "EmbeddingsMetadata" + DROP COLUMN "models", + ADD COLUMN "language" "ISO6391Enum", + ALTER COLUMN "refId" SET NOT NULL; + ALTER TABLE "EmbeddingsJobQueue" + ADD COLUMN "retryAfter" TIMESTAMP WITH TIME ZONE, + ADD COLUMN "retryCount" SMALLINT NOT NULL DEFAULT 0, + ADD COLUMN "startAt" TIMESTAMP WITH TIME ZONE, + ADD COLUMN "priority" SMALLINT NOT NULL DEFAULT 50, + ADD COLUMN "jobData" JSONB NOT NULL DEFAULT '{}', + ADD COLUMN "jobType" VARCHAR(255) NOT NULL, + DROP CONSTRAINT IF EXISTS "EmbeddingsJobQueue_objectType_refId_model_key", + DROP COLUMN "refId", + DROP COLUMN "objectType", + DROP COLUMN "model"; + CREATE INDEX IF NOT EXISTS "idx_EmbeddingsJobQueue_priority" ON "EmbeddingsJobQueue"("priority"); + `) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TYPE IF EXISTS "ISO6391Enum" CASCADE; + DROP INDEX IF EXISTS "idx_Discussion_createdAt"; + ALTER TYPE "EmbeddingsJobStateEnum" RENAME VALUE 'running' TO 'embedding'; + ALTER TYPE "EmbeddingsJobStateEnum" RENAME TO "EmbeddingsStateEnum"; + ALTER TABLE "EmbeddingsMetadata" + ADD COLUMN "models" VARCHAR(255)[], + DROP COLUMN IF EXISTS "language", + ALTER COLUMN "refId" DROP NOT NULL; + ALTER TABLE "EmbeddingsJobQueue" + ALTER COLUMN "state" TYPE "EmbeddingsStateEnum" USING "state"::text::"EmbeddingsStateEnum", + ADD CONSTRAINT "EmbeddingsJobQueue_objectType_refId_model_key" UNIQUE("objectType", "refId", "model"), + ADD COLUMN "refId" VARCHAR(100), + ADD COLUMN "model" VARCHAR(255), + ADD COLUMN "objectType" "EmbeddingsObjectTypeEnum", + DROP COLUMN "retryAfter", + DROP COLUMN "retryCount", + DROP COLUMN "startAt", + DROP COLUMN "jobData", + DROP COLUMN "priority", + DROP COLUMN "jobType"; + DROP INDEX IF EXISTS "idx_EmbeddingsJobQueue_priority"; + `) + await client.end() +} diff --git a/packages/server/postgres/types/Meeting.ts b/packages/server/postgres/types/Meeting.d.ts similarity index 100% rename from packages/server/postgres/types/Meeting.ts rename to packages/server/postgres/types/Meeting.d.ts diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index a1a9c3130df..bc89d53cbbe 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -8,24 +8,16 @@ "parabol-client/*": ["client/*"], "~/*": ["client/*"] }, - "lib": ["esnext", "dom"], - "types": ["node", "jest", "jest-extended"] + "lib": ["esnext", "dom"] }, "exclude": [ "**/node_modules", "types/githubTypes.ts", "postgres/migrationTemplate.ts", - "graphql/intranetSchema/sdl/resolverTypes.ts" - ], - "files": [ - "types/webpackEnv.ts", - "types/modules.d.ts", - "server.ts", - "../client/modules/email/components/SummaryEmail/MeetingSummaryEmail/MeetingSummaryEmail.tsx" - ], - "include": ["graphql/**/*.ts"] - - // if "include" or "files" is added, even if they are empty arrays, then strictNullChecks breaks - // repro: https://www.typescriptlang.org/play?strictFunctionTypes=false&strictPropertyInitialization=false&strictBindCallApply=false&noImplicitThis=false&noImplicitReturns=false&alwaysStrict=false&declaration=false&experimentalDecorators=false&emitDecoratorMetadata=false&target=6&ts=3.5.1#code/C4TwDgpgBA8gRgKygXigbwFBSgWwIYhwQDKwATgIJlkD8AXFAM7kCWAdgOYDaAuhgL4ZQkKFTIpYiLgHJ8hEuTHS+AYwD2bZlDwS2AVwA2B7Y21sQJ0dQx4AdADM1ZAKJ4VACwAUngF4BKFAA+KH8MIA + "database/migrations/**/*", + "postgres/migrations/**/*", + "graphql/intranetSchema/sdl/resolverTypes.ts", + "billing/debug.ts" + ] } diff --git a/packages/server/types/custom.d.ts b/packages/server/types/custom.d.ts index 34587f968c4..aae67c270cd 100644 --- a/packages/server/types/custom.d.ts +++ b/packages/server/types/custom.d.ts @@ -1,3 +1,9 @@ +import {GraphQLSchema} from 'graphql' +import type nestGitHubEndpoint from 'nest-graphql-endpoint/lib/nestGitHubEndpoint' +import '../../client/types/reactHTML4' +import type AuthToken from '../database/types/AuthToken' +import type ScheduledJobMeetingStageTimeLimit from '../database/types/ScheduledJobMetingStageTimeLimit' +import type ScheduledTeamLimitsJob from '../database/types/ScheduledTeamLimitsJob' export interface OAuth2Success { access_token: string token_type: string @@ -17,3 +23,26 @@ export interface OAuth2Error { error_description?: string error_uri?: string } +export interface GQLRequest { + authToken: AuthToken + ip?: string + socketId?: string + variables?: {[key: string]: any} + docId?: string + query?: string + rootValue?: {[key: string]: any} + dataLoaderId?: string + // true if the query is on the private schema + isPrivate?: boolean + // true if the query is ad-hoc (e.g. GraphiQL, CLI) + isAdHoc?: boolean + // Datadog opentracing span of the calling server + carrier?: any +} + +export type ScheduledJobUnion = ScheduledJobMeetingStageTimeLimit | ScheduledTeamLimitsJob + +export type RootSchema = GraphQLSchema & { + githubRequest: ReturnType['githubRequest'] + gitlabRequest: ReturnType['githubRequest'] +} diff --git a/packages/server/types/modules.d.ts b/packages/server/types/modules.d.ts index c79e5a0b011..9b16b604830 100644 --- a/packages/server/types/modules.d.ts +++ b/packages/server/types/modules.d.ts @@ -21,6 +21,7 @@ declare module 'node-env-flag' declare module '*getProjectRoot' declare module 'tayden-clusterfck' declare module 'unicode-substring' +declare module 'jest-extended' declare module 'json2csv/lib/JSON2CSVParser' declare module 'object-hash' declare module 'string-score' diff --git a/packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts b/packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts index 3c65dff93cb..6858d2854cf 100644 --- a/packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts +++ b/packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts @@ -1,16 +1,16 @@ /* eslint-env jest */ -import {MasterPool, r} from 'rethinkdb-ts' -import getRedis from '../getRedis' -import RedisLockQueue from '../RedisLockQueue' -import sleep from 'parabol-client/utils/sleep' +import {r} from 'rethinkdb-ts' import getRethinkConfig from '../../database/getRethinkConfig' import getRethink from '../../database/rethinkDriver' -import {getEligibleOrgIdsByDomain} from '../isRequestToJoinDomainAllowed' +import OrganizationUser from '../../database/types/OrganizationUser' import generateUID from '../../generateUID' +import {DataLoaderWorker} from '../../graphql/graphql' +import getRedis from '../getRedis' +import {getEligibleOrgIdsByDomain} from '../isRequestToJoinDomainAllowed' jest.mock('../../database/rethinkDriver') -getRethink.mockImplementation(() => { - return r +jest.mocked(getRethink).mockImplementation(() => { + return r as any }) const TEST_DB = 'isRequestToJoinDomainAllowedTest' @@ -21,7 +21,7 @@ const testConfig = { db: TEST_DB } -const createTables = async (...tables: string) => { +const createTables = async (...tables: string[]) => { for (const tableName of tables) { const structure = await r .db('rethinkdb') @@ -37,9 +37,8 @@ const createTables = async (...tables: string) => { } } -type TestOrganizationUser = Pick< - OrganizationUser, - 'inactive' | 'joinedAt' | 'removedAt' | 'role' | 'userId' +type TestOrganizationUser = Partial< + Pick > const addOrg = async ( @@ -88,12 +87,12 @@ const dataLoader = { users: userLoader, isCompanyDomain: isCompanyDomainLoader } - return loaders[loader] + return loaders[loader as keyof typeof loaders] }) -} +} as any as DataLoaderWorker beforeAll(async () => { - const conn = await r.connectPool(testConfig) + await r.connectPool(testConfig) try { await r.dbDrop(TEST_DB).run() } catch (e) { @@ -109,7 +108,7 @@ afterEach(async () => { }) afterAll(async () => { - await r.getPoolMaster().drain() + await r.getPoolMaster()?.drain() getRedis().quit() }) @@ -126,7 +125,7 @@ test('Founder is billing lead', async () => { } ]) - const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) + await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) expect(userLoader.loadMany).toHaveBeenCalledTimes(1) expect(userLoader.loadMany).toHaveBeenCalledWith(['user1']) }) @@ -148,7 +147,7 @@ test('Org with noPromptToJoinOrg feature flag is ignored', async () => { {featureFlags: ['noPromptToJoinOrg']} ) - const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) + await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) expect(userLoader.loadMany).toHaveBeenCalledTimes(0) }) @@ -170,7 +169,7 @@ test('Inactive founder is ignored', async () => { } ]) - const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) + await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) // implementation detail, important is only that no user was loaded expect(userLoader.loadMany).toHaveBeenCalledTimes(1) expect(userLoader.loadMany).toHaveBeenCalledWith([]) @@ -195,7 +194,7 @@ test('Non-founder billing lead is checked', async () => { } ]) - const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) + await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) expect(userLoader.loadMany).toHaveBeenCalledTimes(1) expect(userLoader.loadMany).toHaveBeenCalledWith(['billing1']) }) @@ -212,7 +211,7 @@ test('Founder is checked even when not billing lead', async () => { } ]) - const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) + await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) expect(userLoader.loadMany).toHaveBeenCalledTimes(1) expect(userLoader.loadMany).toHaveBeenCalledWith(['user1']) }) @@ -239,7 +238,7 @@ test('All matching orgs are checked', async () => { } ]) - const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) + await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) // implementation detail, important is only that both users were loaded expect(userLoader.loadMany).toHaveBeenCalledTimes(2) expect(userLoader.loadMany).toHaveBeenCalledWith(['founder1']) @@ -249,12 +248,12 @@ test('All matching orgs are checked', async () => { test('Empty org does not throw', async () => { await addOrg('parabol.co', []) - const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) + await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) expect(userLoader.loadMany).toHaveBeenCalledTimes(0) }) test('No org does not throw', async () => { - const orgIds = await getEligibleOrgIdsByDomain('example.com', 'newUser', dataLoader) + await getEligibleOrgIdsByDomain('example.com', 'newUser', dataLoader) expect(userLoader.loadMany).toHaveBeenCalledTimes(0) }) @@ -267,7 +266,7 @@ test('1 person orgs are ignored', async () => { } ]) - const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) + await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) expect(userLoader.loadMany).toHaveBeenCalledTimes(0) }) @@ -283,12 +282,12 @@ test('Org matching the user are ignored', async () => { } ]) - const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) + await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) expect(userLoader.loadMany).toHaveBeenCalledTimes(0) }) test('Only the biggest org with verified emails qualify', async () => { - const org = await addOrg('parabol.co', [ + await addOrg('parabol.co', [ { joinedAt: new Date('2023-09-06'), userId: 'founder1' @@ -350,7 +349,7 @@ test('Only the biggest org with verified emails qualify', async () => { ] } } - return userIds.map((id) => ({ + return userIds.map((id: keyof typeof users) => ({ id, ...users[id] })) @@ -423,7 +422,7 @@ test('All the biggest orgs with verified emails qualify', async () => { ] } } - return userIds.map((id) => ({ + return userIds.map((id: keyof typeof users) => ({ id, ...users[id] })) @@ -452,7 +451,7 @@ test('Team trumps starter tier with more users org', async () => { ], {tier: 'team'} ) - const biggerStarterOrg = await addOrg('parabol.co', [ + await addOrg('parabol.co', [ { joinedAt: new Date('2023-09-06'), userId: 'founder2' @@ -504,7 +503,7 @@ test('Team trumps starter tier with more users org', async () => { ] } } - return userIds.map((id) => ({ + return userIds.map((id: keyof typeof users) => ({ id, ...users[id] })) @@ -533,7 +532,7 @@ test('Enterprise trumps team tier with more users org', async () => { ], {tier: 'enterprise'} ) - const starterOrg = await addOrg( + await addOrg( 'parabol.co', [ { @@ -589,7 +588,7 @@ test('Enterprise trumps team tier with more users org', async () => { ] } } - return userIds.map((id) => ({ + return userIds.map((id: keyof typeof users) => ({ id, ...users[id] })) @@ -604,7 +603,7 @@ test('Enterprise trumps team tier with more users org', async () => { }) test('Orgs with verified emails from different domains do not qualify', async () => { - const org1 = await addOrg('parabol.co', [ + await addOrg('parabol.co', [ { joinedAt: new Date('2023-09-06'), userId: 'founder1' diff --git a/packages/server/utils/__tests__/rateLimiters/InMemoryRateLimiter.test.ts b/packages/server/utils/__tests__/rateLimiters/InMemoryRateLimiter.test.ts index 01f4fa4394a..3b2478aa9d6 100644 --- a/packages/server/utils/__tests__/rateLimiters/InMemoryRateLimiter.test.ts +++ b/packages/server/utils/__tests__/rateLimiters/InMemoryRateLimiter.test.ts @@ -14,7 +14,7 @@ describe('InMemoryRateLimiter', () => { } beforeEach(() => { - jest.useFakeTimers('modern') + jest.useFakeTimers() }) afterEach(() => { diff --git a/packages/server/utils/analytics/analytics.ts b/packages/server/utils/analytics/analytics.ts index a4cbc61f189..305839e2651 100644 --- a/packages/server/utils/analytics/analytics.ts +++ b/packages/server/utils/analytics/analytics.ts @@ -5,9 +5,12 @@ import Meeting from '../../database/types/Meeting' import MeetingMember from '../../database/types/MeetingMember' import MeetingRetrospective from '../../database/types/MeetingRetrospective' import MeetingTemplate from '../../database/types/MeetingTemplate' -import {Reactable} from '../../database/types/Reactable' +import {Reactable, ReactableEnum} from '../../database/types/Reactable' +import {SlackNotificationEventEnum} from '../../database/types/SlackNotification' import {TaskServiceEnum} from '../../database/types/Task' -import {ReactableEnum} from '../../graphql/private/resolverTypes' +import TemplateScale from '../../database/types/TemplateScale' +import {DataLoaderWorker} from '../../graphql/graphql' +import {ModifyType} from '../../graphql/public/resolverTypes' import {IntegrationProviderServiceEnumType} from '../../graphql/types/IntegrationProviderServiceEnum' import {UpgradeCTALocationEnumType} from '../../graphql/types/UpgradeCTALocationEnum' import {TeamPromptResponse} from '../../postgres/queries/getTeamPromptResponsesByIds' @@ -16,10 +19,6 @@ import {MeetingTypeEnum} from '../../postgres/types/Meeting' import {MeetingSeries} from '../../postgres/types/MeetingSeries' import {AmplitudeAnalytics} from './amplitude/AmplitudeAnalytics' import {createMeetingProperties} from './helpers' -import {SlackNotificationEventEnum} from '../../database/types/SlackNotification' -import TemplateScale from '../../database/types/TemplateScale' -import {DataLoaderWorker} from '../../graphql/graphql' -import {ModifyType} from '../../graphql/public/resolverTypes' export type AnalyticsUser = { id: string diff --git a/packages/server/utils/getGitHubRequest.ts b/packages/server/utils/getGitHubRequest.ts index 76efb0db5ae..59ff1eea559 100644 --- a/packages/server/utils/getGitHubRequest.ts +++ b/packages/server/utils/getGitHubRequest.ts @@ -1,6 +1,6 @@ import {GraphQLResolveInfo} from 'graphql' import {EndpointContext} from 'nest-graphql-endpoint/lib/types' -import {RootSchema} from '../graphql/public/rootSchema' +import {RootSchema} from '../types/custom' // This helper just cleans up the input/output boilerplate. // It breaks githubRequest into 2 parts so the info, endpointContext, and batchRef are kept in context diff --git a/packages/server/utils/getGraphQLExecutor.ts b/packages/server/utils/getGraphQLExecutor.ts index bc70b875316..759f21bade2 100644 --- a/packages/server/utils/getGraphQLExecutor.ts +++ b/packages/server/utils/getGraphQLExecutor.ts @@ -1,7 +1,7 @@ import {ExecutionResult} from 'graphql' import {ServerChannel} from 'parabol-client/types/constEnums' +import type {GQLRequest} from '../types/custom' import SocketServerChannelId from '../../client/shared/gqlIds/SocketServerChannelId' -import {GQLRequest} from '../graphql/executeGraphQL' import PubSubPromise from './PubSubPromise' let pubsub: PubSubPromise diff --git a/packages/server/utils/uwsGetHeaders.ts b/packages/server/utils/uwsGetHeaders.ts index 9fd7bdcdc02..f51fd27e78f 100644 --- a/packages/server/utils/uwsGetHeaders.ts +++ b/packages/server/utils/uwsGetHeaders.ts @@ -1,7 +1,7 @@ import {HttpRequest} from 'uWebSockets.js' const uwsGetHeaders = (req: HttpRequest) => { - const reqHeaders = {} + const reqHeaders: Record = {} req.forEach((key, value) => { reqHeaders[key] = value }) diff --git a/release-please-config.json b/release-please-config.json index 29f302183a1..b8959848b5a 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -60,6 +60,11 @@ "path": "packages/integration-tests/package.json", "jsonpath": "$.version" }, + { + "type": "json", + "path": "packages/embedder/package.json", + "jsonpath": "$.version" + }, { "type": "json", "path": "packages/server/package.json", diff --git a/scripts/webpack/utils/getProjectRoot.d.ts b/scripts/webpack/utils/getProjectRoot.d.ts new file mode 100644 index 00000000000..436f1feb4c9 --- /dev/null +++ b/scripts/webpack/utils/getProjectRoot.d.ts @@ -0,0 +1,2 @@ +declare function getProjectRoot(): string +export default getProjectRoot diff --git a/tsconfig.base.json b/tsconfig.base.json index adc1623c1b5..c207d6f1c07 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -16,6 +16,7 @@ "strictPropertyInitialization": true, "noImplicitThis": true, "alwaysStrict": true, + "noEmit": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, diff --git a/yarn.lock b/yarn.lock index dc34fd16fae..6701230bae9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + "@amplitude/analytics-browser@^2.2.3": version "2.2.3" resolved "https://registry.yarnpkg.com/@amplitude/analytics-browser/-/analytics-browser-2.2.3.tgz#7dbe4ad2ada7facfcf21aac4b47314f8e4c2903c" @@ -3166,33 +3171,38 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb" integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg== -"@eslint-community/eslint-utils@^4.4.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.5.1": +"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": version "4.10.0" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== -"@eslint/eslintrc@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" - integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ== +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.2.0" - globals "^13.9.0" - ignore "^4.0.6" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" - minimatch "^3.0.4" + minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== + "@exodus/schemasafe@^1.0.0-rc.2": version "1.0.1" resolved "https://registry.yarnpkg.com/@exodus/schemasafe/-/schemasafe-1.0.1.tgz#e4e2d86ae176b7c96fbff033f3b1a8b1cfd390fb" @@ -3833,24 +3843,29 @@ dependencies: client-only "^0.0.1" -"@humanwhocodes/config-array@^0.9.2": - version "0.9.2" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914" - integrity sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA== +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.4" + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== "@humanwhocodes/momoa@^2.0.3": version "2.0.4" resolved "https://registry.yarnpkg.com/@humanwhocodes/momoa/-/momoa-2.0.4.tgz#8b9e7a629651d15009c3587d07a222deeb829385" integrity sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA== -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" + integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== "@hutson/parse-repository-url@^3.0.0": version "3.0.2" @@ -5065,7 +5080,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3": +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -7565,6 +7580,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/faker@^5.5.9": + version "5.5.9" + resolved "https://registry.yarnpkg.com/@types/faker/-/faker-5.5.9.tgz#588ede92186dc557bff8341d294335d50d255f0c" + integrity sha512-uCx6mP3UY5SIO14XlspxsGjgaemrxpssJI0Ol+GfhxtcKpv9pgRZYsS4eeKeHVLje6Qtc8lGszuBI461+gVZBA== + "@types/fbjs@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/fbjs/-/fbjs-3.0.4.tgz#272faf44b6a24d24d6fdf36ed895b47436e6c125" @@ -7572,6 +7592,11 @@ dependencies: "@types/jsdom" "*" +"@types/franc@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/franc/-/franc-5.0.3.tgz#7263cef3ab3512ac95a78c328fcc51c51396b49f" + integrity sha512-YX6o2vVkeiUvOF12bUmnSGf8sezOoBnCWjHHZGeh2lt3tqAutbJ9OL3cDRiZoiAYaZR638nuOc0Ji9bzdad2XA== + "@types/glob@*": version "8.1.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc" @@ -7674,6 +7699,14 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/jest@^29.5.12": + version "29.5.12" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544" + integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + "@types/js-yaml@^4.0.0": version "4.0.5" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" @@ -8190,16 +8223,16 @@ resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e" integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g== -"@typescript-eslint/eslint-plugin@^6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" - integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== +"@typescript-eslint/eslint-plugin@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz#de61c3083842fc6ac889d2fc83c9a96b55ab8328" + integrity sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/type-utils" "6.21.0" - "@typescript-eslint/utils" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/scope-manager" "7.4.0" + "@typescript-eslint/type-utils" "7.4.0" + "@typescript-eslint/utils" "7.4.0" + "@typescript-eslint/visitor-keys" "7.4.0" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -8207,47 +8240,47 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" - integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== +"@typescript-eslint/parser@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.4.0.tgz#540f4321de1e52b886c0fa68628af1459954c1f1" + integrity sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ== dependencies: - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/scope-manager" "7.4.0" + "@typescript-eslint/types" "7.4.0" + "@typescript-eslint/typescript-estree" "7.4.0" + "@typescript-eslint/visitor-keys" "7.4.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== +"@typescript-eslint/scope-manager@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz#acfc69261f10ece7bf7ece1734f1713392c3655f" + integrity sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/types" "7.4.0" + "@typescript-eslint/visitor-keys" "7.4.0" -"@typescript-eslint/type-utils@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" - integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== +"@typescript-eslint/type-utils@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz#cfcaab21bcca441c57da5d3a1153555e39028cbd" + integrity sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw== dependencies: - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/utils" "6.21.0" + "@typescript-eslint/typescript-estree" "7.4.0" + "@typescript-eslint/utils" "7.4.0" debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== +"@typescript-eslint/types@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.4.0.tgz#ee9dafa75c99eaee49de6dcc9348b45d354419b6" + integrity sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw== -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== +"@typescript-eslint/typescript-estree@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz#12dbcb4624d952f72c10a9f4431284fca24624f4" + integrity sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/types" "7.4.0" + "@typescript-eslint/visitor-keys" "7.4.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -8255,27 +8288,32 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" - integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== +"@typescript-eslint/utils@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.4.0.tgz#d889a0630cab88bddedaf7c845c64a00576257bd" + integrity sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/scope-manager" "7.4.0" + "@typescript-eslint/types" "7.4.0" + "@typescript-eslint/typescript-estree" "7.4.0" semver "^7.5.4" -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== +"@typescript-eslint/visitor-keys@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz#0c8ff2c1f8a6fe8d7d1a57ebbd4a638e86a60a94" + integrity sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA== dependencies: - "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/types" "7.4.0" eslint-visitor-keys "^3.4.1" +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + "@webassemblyjs/ast@1.11.5", "@webassemblyjs/ast@^1.11.5": version "1.11.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.5.tgz#6e818036b94548c1fb53b754b5cae3c9b208281c" @@ -8672,7 +8710,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -9704,9 +9742,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001580, caniuse-lite@~1.0.0: - version "1.0.30001594" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001594.tgz#bea552414cd52c2d0c985ed9206314a696e685f5" - integrity sha512-VblSX6nYqyJVs8DKFMldE2IVCJjZ225LW00ydtUWwh5hk9IfkTOffO6r8gJNsH0qqqeAF8KrbMYA2VEwTlGW5g== + version "1.0.30001603" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001603.tgz#605046a5bdc95ba4a92496d67e062522dce43381" + integrity sha512-iL2iSS0eDILMb9n5yKQoTBim9jMZ0Yrk8g0N9K7UzYyWnfIKzXBZD5ngpM37ZcL/cv0Mli8XtVMRYMQAfFpi5Q== capital-case@^1.0.4: version "1.0.4" @@ -10096,6 +10134,11 @@ codemirror@^5.65.3: resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.3.tgz#2d029930d5a293bc5fb96ceea64654803c0d4ac7" integrity sha512-kCC0iwGZOVZXHEKW3NDTObvM7pTIyowjty4BUqeREROc/3I6bWbgZDA3fGDwlA+rbgRjvnRnfqs9SfXynel1AQ== +collapse-white-space@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" + integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" @@ -11305,7 +11348,6 @@ draft-js-utils@^1.4.0: "draft-js@https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6": version "0.10.5" - uid "025fddba56f21aaf3383aee778e0b17025c9a7bc" resolved "https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6#025fddba56f21aaf3383aee778e0b17025c9a7bc" dependencies: fbjs "^0.8.15" @@ -11639,10 +11681,10 @@ escodegen@^2.0.0, escodegen@^2.1.0: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" - integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== +eslint-config-prettier@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== eslint-plugin-emotion@^10.0.14: version "10.0.27" @@ -11682,71 +11724,62 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153" - integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg== +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.2.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8.2.0, eslint@^8.8.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.8.0.tgz#9762b49abad0cb4952539ffdb0a046392e571a2d" - integrity sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ== - dependencies: - "@eslint/eslintrc" "^1.0.5" - "@humanwhocodes/config-array" "^0.9.2" - ajv "^6.10.0" +eslint@^8.57.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.1.0" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.2.0" - espree "^9.3.0" - esquery "^1.4.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^6.0.1" - globals "^13.6.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" ignore "^5.2.0" - import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" + is-path-inside "^3.0.3" js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" + optionator "^0.9.3" strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" text-table "^0.2.0" - v8-compile-cache "^2.0.3" esniff@^2.0.1: version "2.0.1" @@ -11758,7 +11791,7 @@ esniff@^2.0.1: event-emitter "^0.3.5" type "^2.7.2" -espree@^9.0.0, espree@^9.2.0, espree@^9.3.0: +espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -11772,13 +11805,20 @@ esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.0.1, esquery@^1.4.0: +esquery@^1.0.1: version "1.4.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== dependencies: estraverse "^5.1.0" +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -12389,6 +12429,13 @@ framesync@6.0.1: dependencies: tslib "^2.1.0" +franc-min@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/franc-min/-/franc-min-5.0.0.tgz#5625d0570a18564dcbbfa8330254d23549294d9a" + integrity sha512-xy7Iq7uNflbvNU+bkyYWtP+BOHWZle7kT9GM84gEV14b7/7sgq7M7Flf6v1XRflHAuHoshBMveWA6Q+kEXYeHQ== + dependencies: + trigram-utils "^1.0.0" + fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -12488,11 +12535,6 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - fuzzy@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/fuzzy/-/fuzzy-0.1.3.tgz#4c76ec2ff0ac1a36a9dccf9a00df8623078d4ed8" @@ -12823,10 +12865,10 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.6.0, globals@^13.9.0: - version "13.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.0.tgz#4d733760304230a0082ed96e21e5c565f898089e" - integrity sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg== +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" @@ -13491,11 +13533,6 @@ ignore-walk@^5.0.1: dependencies: minimatch "^5.0.1" -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - ignore@^5.0.4, ignore@^5.1.4, ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -13531,7 +13568,7 @@ import-fresh@^2.0.0: caller-path "^2.0.0" resolve-from "^3.0.0" -import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: +import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -13925,6 +13962,11 @@ is-path-inside@^2.1.0: dependencies: path-is-inside "^1.0.2" +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -15014,10 +15056,10 @@ kysely-codegen@^0.11.0: micromatch "^4.0.5" minimist "^1.2.8" -kysely@^0.27.2: - version "0.27.2" - resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.27.2.tgz#b289ce5e561064ec613a17149b7155783d2b36de" - integrity sha512-DmRvEfiR/NLpgsTbSxma2ldekhsdcd65+MNiKXyd/qj7w7X5e3cLkXxcj+MypsRDjPhHQ/CD5u3Eq1sBYzX0bw== +kysely@^0.27.3: + version "0.27.3" + resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.27.3.tgz#6cc6c757040500b43c4ac596cdbb12be400ee276" + integrity sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA== launch-editor@^2.6.0: version "2.6.0" @@ -15883,7 +15925,7 @@ minimatch@9.0.3: dependencies: brace-expansion "^2.0.1" -minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5: +minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -16127,6 +16169,11 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +n-gram@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/n-gram/-/n-gram-1.1.2.tgz#69c609a5c83bb32f82774c9e297f8494c7326798" + integrity sha512-mBTpWKp0NHdujHmxrskPg2jc108mjyMmVxHN1rZGK/ogTLi9O0debDIXlQPqotNELdNmVGtL4jr7SCig+4OWvQ== + nan@^2.17.0: version "2.18.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" @@ -16864,11 +16911,23 @@ openai@^4.24.1: node-fetch "^2.6.7" web-streams-polyfill "^3.2.1" +openapi-fetch@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/openapi-fetch/-/openapi-fetch-0.9.3.tgz#37c1dbde7faec885eaa40f351cab1c231b794761" + integrity sha512-tC1NDn71vJHeCzu+lYdrnIpgRt4GxR0B4eSwXNb15ypWpZcpaEOwHFkoz8FcfG5Fvqkz2P0Fl9zQF1JJwBjuvA== + dependencies: + openapi-typescript-helpers "^0.0.7" + openapi-types@^12.1.0: version "12.1.1" resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.1.tgz#0aface4e05ba60efbf51153ed6af23988796617d" integrity sha512-m/DJaEqOUDSU8KoI74E6A3TokccuDOJ81ewZ6kLFwUT1KEIE0GDWvErtnJJDU4sySx8JKF5kk2GzHUuK6f+VHA== +openapi-typescript-helpers@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.7.tgz#1d0ead67c35864d189c2cb2d0556854ccbb16c38" + integrity sha512-7nwlAtdA1fULipibFRBWE/rnF114q6ejRYzNvhdA/x+qTWAZhXGLc/368dlwMlyJDvCQMCnADjpzb5BS5ZmNSA== + opener@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" @@ -16891,17 +16950,17 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" ora@5.4.1, ora@^5.4.1: version "5.4.1" @@ -18804,11 +18863,6 @@ regexp.prototype.flags@^1.3.1: call-bind "^1.0.2" define-properties "^1.1.3" -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - regexpu-core@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.1.0.tgz#2f8504c3fd0ebe11215783a41541e21c79942c6d" @@ -20384,7 +20438,19 @@ tar@^4.4.13: safe-buffer "^5.2.1" yallist "^3.1.1" -tar@^6.0.2, tar@^6.1.0, tar@^6.1.11, tar@^6.1.2: +tar@^6.0.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tar@^6.1.0, tar@^6.1.11, tar@^6.1.2: version "6.2.0" resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73" integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ== @@ -20680,6 +20746,15 @@ treeverse@^2.0.0: resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-2.0.0.tgz#036dcef04bc3fd79a9b79a68d4da03e882d8a9ca" integrity sha512-N5gJCkLu1aXccpOTtqV6ddSEi6ZmGkh3hjmbu1IjcavJK4qyOVQmi0myQKM7z5jVGmD68SJoliaVrMmVObhj6A== +trigram-utils@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/trigram-utils/-/trigram-utils-1.0.3.tgz#535da37a414dae249c4b023512cf2b3dc65c8ea4" + integrity sha512-UAhS1Ll21FtClVIzIN0I/SmGnJ+D08BOxX7Dl1penV8raC0ksf2dJkhNI6kU1Mj3uT86Bul12iMvxXquXSYSng== + dependencies: + collapse-white-space "^1.0.0" + n-gram "^1.0.0" + trim "0.0.1" + trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" @@ -20690,6 +20765,11 @@ trim-newlines@^4.0.2: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.0.2.tgz#d6aaaf6a0df1b4b536d183879a6b939489808c7c" integrity sha512-GJtWyq9InR/2HRiLZgpIKv+ufIKrVrvjQWEj7PxAXNc5dwbNJkqhAUoAGgzRmULAnoOM5EIpveYd3J2VeSAIew== +trim@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + integrity sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ== + ts-algebra@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/ts-algebra/-/ts-algebra-1.2.0.tgz#f91c481207a770f0d14d055c376cbee040afdfc9" @@ -20973,10 +21053,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@4.9.5, "typescript@^3 || ^4", typescript@^4.2.4, typescript@^5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +"typescript@^3 || ^4", typescript@^4.2.4, typescript@^5.3.3: + version "5.4.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372" + integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ== uWebSockets.js@uNetworking/uWebSockets.js#v20.34.0: version "20.34.0" @@ -21297,7 +21377,7 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -v8-compile-cache@2.3.0, v8-compile-cache@^2.0.3: +v8-compile-cache@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== @@ -21755,7 +21835,7 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== -word-wrap@^1.2.3, word-wrap@~1.2.3: +word-wrap@~1.2.3: version "1.2.4" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== From 8b42a5ddf42d8a416387b57fa8b23173e164e509 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 09:53:33 +0200 Subject: [PATCH 10/19] chore(release): Test v7.25.4 (#9616) Signed-off-by: dependabot[bot] Signed-off-by: Matt Krick Co-authored-by: Rafa <101704572+rafaelromcar-parabol@users.noreply.github.com> Co-authored-by: Georg Bremer Co-authored-by: Nick O'Ferrall Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: parabol-release-bot[bot] <150284312+parabol-release-bot[bot]@users.noreply.github.com> Co-authored-by: Matt Krick Co-authored-by: Jordan Husney Co-authored-by: adaniels-parabol <71724289+adaniels-parabol@users.noreply.github.com> Co-authored-by: snyk-bot Co-authored-by: Dale Bumblis <135627447+dbumblis-parabol@users.noreply.github.com> Co-authored-by: Bartosz Jarocki Co-authored-by: Marcus Wermuth Co-authored-by: Rafael Romero Co-authored-by: Bruce Tian Co-authored-by: GitHub Action Co-authored-by: Mohd Muneeb Co-authored-by: Muneeb-Ventures Co-authored-by: github-actions --- .env.example | 1 - .github/workflows/ironbank.yml | 1 + .github/workflows/release-to-staging.yml | 2 +- .release-please-manifest.json | 2 +- CHANGELOG.md | 67 +++++++ docker/stacks/single-tenant-host/README.md | 2 +- package.json | 2 +- packages/chronos/package.json | 4 +- .../ActivityDetails/TemplateDetails.tsx | 19 +- .../ActivityDetailsSidebar.tsx | 143 +++------------ .../ActivityLibrary/ActivityLibrary.tsx | 5 - .../ActivityLibrary/OneOnOneTeamStatus.tsx | 28 --- .../OneOnOneTeamStatusComponent.tsx | 40 ----- packages/client/components/Avatar/Avatar.tsx | 12 +- .../client/components/MeetingControlBar.tsx | 3 +- packages/client/components/TeamHealth.tsx | 4 + .../components/InvoiceRow/InvoiceRow.tsx | 5 +- .../components/OrgBilling/OrgPlans.tsx | 11 +- .../client/mutations/StartCheckInMutation.ts | 8 +- packages/client/package.json | 8 +- .../shared/gqlIds/DomainJoinRequestId.ts | 23 ++- packages/client/utils/AtlassianManager.ts | 1 - .../client/utils/JiraServerClientManager.ts | 1 + packages/client/utils/constants.ts | 12 +- packages/client/utils/makeMonthDateString.ts | 10 -- packages/embedder/package.json | 2 +- packages/gql-executor/package.json | 6 +- packages/integration-tests/package.json | 2 +- packages/server/database/types/Team.ts | 4 - .../server/email/teamInviteEmailCreator.tsx | 23 +-- .../server/graphql/mutations/endCheckIn.ts | 2 +- .../mutations/helpers/createTeamAndLeader.ts | 3 +- .../helpers/getExistingOneOnOneTeam.ts | 43 ----- .../mutations/helpers/importTasksForPoker.ts | 2 +- .../helpers/maybeCreateOneOnOneTeam.ts | 98 ---------- .../server/graphql/mutations/joinMeeting.ts | 3 +- .../typeDefs/updateOrgFeatureFlag.graphql | 1 - .../graphql/public/mutations/startCheckIn.ts | 33 +--- .../typeDefs/CreateOneOnOneTeamInput.graphql | 4 - .../public/typeDefs/Organization.graphql | 6 - .../graphql/public/typeDefs/_legacy.graphql | 16 ++ .../public/typeDefs/startCheckIn.graphql | 6 +- .../public/types/CreateOneOnOneTeamInput.ts | 21 --- .../graphql/public/types/Organization.ts | 33 +--- .../public/types/OrganizationFeatureFlags.ts | 1 - .../queries/helpers/makeUpcomingInvoice.ts | 28 ++- packages/server/graphql/queries/invoices.ts | 3 +- packages/server/graphql/rootTypes.ts | 2 + packages/server/package.json | 5 +- .../1712073121060_removeOneOnOne.ts | 16 ++ .../postgres/queries/src/insertTeamQuery.sql | 2 - packages/server/utils/analytics/analytics.ts | 33 +--- packages/server/utils/analytics/helpers.ts | 7 +- packages/server/utils/uwsGetIP.ts | 2 +- .../meeting_types-one_on_one.svg | 28 --- static/images/illustrations/oneOnOne.svg | 33 ---- yarn.lock | 170 +++++++++--------- 57 files changed, 346 insertions(+), 706 deletions(-) delete mode 100644 packages/client/components/ActivityLibrary/OneOnOneTeamStatus.tsx delete mode 100644 packages/client/components/ActivityLibrary/OneOnOneTeamStatusComponent.tsx delete mode 100644 packages/client/utils/makeMonthDateString.ts delete mode 100644 packages/server/graphql/mutations/helpers/getExistingOneOnOneTeam.ts delete mode 100644 packages/server/graphql/mutations/helpers/maybeCreateOneOnOneTeam.ts delete mode 100644 packages/server/graphql/public/typeDefs/CreateOneOnOneTeamInput.graphql delete mode 100644 packages/server/graphql/public/types/CreateOneOnOneTeamInput.ts create mode 100644 packages/server/postgres/migrations/1712073121060_removeOneOnOne.ts delete mode 100644 static/images/illustrations/meeting_types-one_on_one.svg delete mode 100644 static/images/illustrations/oneOnOne.svg diff --git a/.env.example b/.env.example index 5f0285650a9..063f74160a9 100644 --- a/.env.example +++ b/.env.example @@ -44,7 +44,6 @@ AI_EMBEDDER_WORKERS='1' # AWS_SECRET_ACCESS_KEY='key_AWS_SECRET_ACCESS_KEY' # MONITORING -# SEGMENT_WRITE_KEY='key_SEGMENT_WRITE_KEY' # SENTRY_DSN='key_SENTRY_DSN' # CDN SETTINGS diff --git a/.github/workflows/ironbank.yml b/.github/workflows/ironbank.yml index e9782493f43..c60df2b134f 100644 --- a/.github/workflows/ironbank.yml +++ b/.github/workflows/ironbank.yml @@ -50,6 +50,7 @@ jobs: run: | docker cp temp-container:/home/node/parabol/dist ./dist docker cp temp-container:/home/node/parabol/build ./build + docker cp temp-container:/home/node/tools/ip-to-server_id ./tools/ip-to-server_id - name: Zip the files run: zip -r ${{ github.event.inputs.version_number }}.zip dist build diff --git a/.github/workflows/release-to-staging.yml b/.github/workflows/release-to-staging.yml index bc30de202b3..329268d89d9 100644 --- a/.github/workflows/release-to-staging.yml +++ b/.github/workflows/release-to-staging.yml @@ -70,7 +70,7 @@ jobs: --header 'PRIVATE-TOKEN: ${{ secrets.GITLAB_DEPLOYMENT_ACCESS_TOKEN }}') PIPELINE_ID=$(echo $PIPELINES | jq ".[] | select(.sha == \"${{ env.COMMIT_ID }}\")" | jq .id) [ -z "$PIPELINE_ID" ] && exit 1 - JOBS=$(curl "https://gitlab.com/api/v4/projects/${{ vars.GITLAB_PROJECT_ID }}/pipelines/$PIPELINE_ID/jobs" \ + JOBS=$(curl "https://gitlab.com/api/v4/projects/${{ vars.GITLAB_PROJECT_ID }}/pipelines/$PIPELINE_ID/jobs?per_page=100" \ --header 'PRIVATE-TOKEN: ${{ secrets.GITLAB_DEPLOYMENT_ACCESS_TOKEN }}') JOB_ID=$(echo $JOBS | jq '.[] | select(.name == "${{ env.STAGING_JOB }}")' | jq .id) PROD_JOB_ID=$(echo $JOBS | jq '.[] | select(.name == "${{ env.PRODUCTION_JOB}}")' | jq .id) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2fb3566ccf3..c7c1488dff8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.24.1" + ".": "7.25.4" } diff --git a/CHANGELOG.md b/CHANGELOG.md index b5ed33f4256..e62ca7d5066 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,73 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.25.4](https://github.com/ParabolInc/parabol/compare/v7.25.3...v7.25.4) (2024-04-09) + + +### Fixed + +* Revert "[Snyk] Upgrade graphql from 15.7.2 to 15.8.0 ([#9569](https://github.com/ParabolInc/parabol/issues/9569))" ([#9614](https://github.com/ParabolInc/parabol/issues/9614)) ([ecc2cdc](https://github.com/ParabolInc/parabol/commit/ecc2cdc4b67e1788763af601984e1cbf76f02235)) + +## [7.25.3](https://github.com/ParabolInc/parabol/compare/v7.25.2...v7.25.3) (2024-04-09) + + +### Fixed + +* Add AuthIdentityMicrosoft ([#9612](https://github.com/ParabolInc/parabol/issues/9612)) ([e3d8b38](https://github.com/ParabolInc/parabol/commit/e3d8b3899845b565e91259217e96f222d116b70c)) +* remove top level graphql-relay dependency ([#9611](https://github.com/ParabolInc/parabol/issues/9611)) ([72fc294](https://github.com/ParabolInc/parabol/commit/72fc2940bb912f9c81056d2d3ac539602a6aa8b2)) + + +### Changed + +* **segment:** remove Segment ([#9599](https://github.com/ParabolInc/parabol/issues/9599)) ([537a8af](https://github.com/ParabolInc/parabol/commit/537a8afce7702251337bc8946a6703c25d668133)) + +## [7.25.2](https://github.com/ParabolInc/parabol/compare/v7.25.1...v7.25.2) (2024-04-08) + + +### Fixed + +* return 100 gitlab pipeline jobs ([#9607](https://github.com/ParabolInc/parabol/issues/9607)) ([7a26009](https://github.com/ParabolInc/parabol/commit/7a26009931fdb55d298390eb7871937d3a09eca7)) + +## [7.25.1](https://github.com/ParabolInc/parabol/compare/v7.25.0...v7.25.1) (2024-04-08) + + +### Fixed + +* fallback for missing avatar images ([#9603](https://github.com/ParabolInc/parabol/issues/9603)) ([1a7e298](https://github.com/ParabolInc/parabol/commit/1a7e298a6fbd4c6a06acdfdca4d815029811178f)) +* fix an issue where upcoming invoice doesn't have the nextPeriodCharges field ([#9580](https://github.com/ParabolInc/parabol/issues/9580)) ([cb52596](https://github.com/ParabolInc/parabol/commit/cb52596c2f69b4b6e1641654610bb2335cefd0f2)) +* fix the issue where timer doesn't work in TEAM_HEALTH phase ([#9597](https://github.com/ParabolInc/parabol/issues/9597)) ([96f29b5](https://github.com/ParabolInc/parabol/commit/96f29b56e7a5bd63cb8ec5c879a7c2fb80ef803d)) +* type error in invite email ([#9606](https://github.com/ParabolInc/parabol/issues/9606)) ([6ead321](https://github.com/ParabolInc/parabol/commit/6ead321741734cff47a953b3b1ac1e8999f16594)) + + +### Changed + +* Add inviter name to invite email subject ([#9604](https://github.com/ParabolInc/parabol/issues/9604)) ([cd5a3a8](https://github.com/ParabolInc/parabol/commit/cd5a3a844a90d5160f11ff38ee63b3db87894b20)) +* update ironbank GH action to copy ip-to-server-id script ([#9594](https://github.com/ParabolInc/parabol/issues/9594)) ([89aeea3](https://github.com/ParabolInc/parabol/commit/89aeea362ac46d365cb2ff9ab9971c30f816103c)) + +## [7.25.0](https://github.com/ParabolInc/parabol/compare/v7.24.1...v7.25.0) (2024-04-04) + + +### Added + +* update pricing page with template changes ([#9596](https://github.com/ParabolInc/parabol/issues/9596)) ([01f69de](https://github.com/ParabolInc/parabol/commit/01f69de9eb809ef16ac954e5d75ac884b11f8342)) + + +### Fixed + +* Add graphql-relay to predeploy ([#9595](https://github.com/ParabolInc/parabol/issues/9595)) ([b92d96e](https://github.com/ParabolInc/parabol/commit/b92d96e2972560ee16f83d90a82ebbb946e39dc0)) +* Don't reuse another team members integrated task ([#9600](https://github.com/ParabolInc/parabol/issues/9600)) ([9794033](https://github.com/ParabolInc/parabol/commit/9794033249ee5c20a8f014a8b5ae38ce87294ead)) +* **single-tenant:** application upgrades do not need --profile databases ([#9593](https://github.com/ParabolInc/parabol/issues/9593)) ([9486587](https://github.com/ParabolInc/parabol/commit/9486587c9f7d4b1c40a0eff549e819ed4565aa23)) +* trim inet address ([#9598](https://github.com/ParabolInc/parabol/issues/9598)) ([c6da00c](https://github.com/ParabolInc/parabol/commit/c6da00c06929d377b8b698c82352559d1da85467)) + + +### Changed + +* **deps-dev:** bump webpack-dev-middleware from 4.0.2 to 5.3.4 ([#9561](https://github.com/ParabolInc/parabol/issues/9561)) ([dbc9f09](https://github.com/ParabolInc/parabol/commit/dbc9f091a4c93efc0eca24c5cd42b80bae95cff3)) +* **deps:** bump express from 4.18.2 to 4.19.2 ([#9566](https://github.com/ParabolInc/parabol/issues/9566)) ([8ab86b4](https://github.com/ParabolInc/parabol/commit/8ab86b4cd8698ba6f0a4cdec8eca2bf31a290599)) +* **deps:** bump follow-redirects from 1.15.2 to 1.15.6 ([#9536](https://github.com/ParabolInc/parabol/issues/9536)) ([e372f5f](https://github.com/ParabolInc/parabol/commit/e372f5f7bccd0a2fd1b4fea414610ad12fe0ec89)) +* **deps:** bump jose from 4.14.4 to 4.15.5 ([#9515](https://github.com/ParabolInc/parabol/issues/9515)) ([c312f48](https://github.com/ParabolInc/parabol/commit/c312f4821698ae7966dee8889a0c8c3353733dcc)) +* Remove one on one meeting type ([#9590](https://github.com/ParabolInc/parabol/issues/9590)) ([415d03b](https://github.com/ParabolInc/parabol/commit/415d03b2ce5216608a2dd144166666013d1752a0)) + ## [7.24.1](https://github.com/ParabolInc/parabol/compare/v7.24.0...v7.24.1) (2024-04-02) diff --git a/docker/stacks/single-tenant-host/README.md b/docker/stacks/single-tenant-host/README.md index 59a6a4a8a3e..5f2ca04254f 100644 --- a/docker/stacks/single-tenant-host/README.md +++ b/docker/stacks/single-tenant-host/README.md @@ -18,7 +18,7 @@ To run Parabol in single tenant mode (e.g. simple docker-compose on a docker hos 1. Edit the `docker-compose.yaml` and change the `#image:tag` changing the tag. Ex: from `v7.15.0` to `v7.15.2`. 2. (optional) In a different terminal, run `docker compose logs -f` to follow the upgrade. -3. Run `docker compose --profile databases --profile parabol up -d`. It will start the `pre-deploy` and, once it is done successfully, then it will stop and recreate the `web-server` and `gql-executor` with the new version of the image. **This step implies a downtime**. +3. Run `docker compose --profile parabol up -d`. It will start the `pre-deploy` and, once it is done successfully, then it will stop and recreate the `web-server` and `gql-executor` with the new version of the image. **This step implies a downtime**. 4. Verify the application is still up and running. ## Running Chronos diff --git a/package.json b/package.json index a6b2e22b178..a389d66f66b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.24.1", + "version": "7.25.4", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/chronos/package.json b/packages/chronos/package.json index b8e0b4886a7..7305021cd2d 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.24.1", + "version": "7.25.4", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -25,6 +25,6 @@ }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.24.1" + "parabol-server": "7.25.4" } } diff --git a/packages/client/components/ActivityLibrary/ActivityDetails/TemplateDetails.tsx b/packages/client/components/ActivityLibrary/ActivityDetails/TemplateDetails.tsx index b86554fb3ef..f45191fad8c 100644 --- a/packages/client/components/ActivityLibrary/ActivityDetails/TemplateDetails.tsx +++ b/packages/client/components/ActivityLibrary/ActivityDetails/TemplateDetails.tsx @@ -73,22 +73,6 @@ const ACTIVITY_TYPE_DATA_LOOKUP: Record< } } -const ACTIVITY_ID_DATA_LOOKUP: Record< - string, - {description: React.ReactNode; integrationsTip: React.ReactNode} -> = { - oneOnOneAction: { - description: ( - <> - This is a space to check in one-on-one. Share a personal update using the Icebreaker{' '} - phase. Give a brief update on what’s changed with your work during the Solo Updates{' '} - phase. Raise issues for discussion in the Agenda phase. - - ), - integrationsTip: <>push takeaway tasks to your backlog - } -} - interface Props { activityRef: TemplateDetails_activity$key isEditing: boolean @@ -143,8 +127,7 @@ export const TemplateDetails = (props: Props) => { } = activity const {id: teamId, editingScaleId} = team - const {description: activityDescription, integrationsTip} = - ACTIVITY_ID_DATA_LOOKUP[activityId] ?? ACTIVITY_TYPE_DATA_LOOKUP[type] + const {description: activityDescription, integrationsTip} = ACTIVITY_TYPE_DATA_LOOKUP[type] const viewer = useFragment( graphql` diff --git a/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx b/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx index 20574555b3b..d0721f72af5 100644 --- a/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx +++ b/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx @@ -16,7 +16,6 @@ import useAtmosphere from '../../hooks/useAtmosphere' import {MenuPosition} from '../../hooks/useCoords' import useMutationProps from '../../hooks/useMutationProps' import SelectTemplateMutation from '../../mutations/SelectTemplateMutation' -import SendClientSideEvent from '../../utils/SendClientSideEvent' import StartCheckInMutation from '../../mutations/StartCheckInMutation' import StartTeamPromptMutation from '../../mutations/StartTeamPromptMutation' import {PALETTE} from '../../styles/paletteV3' @@ -33,14 +32,6 @@ import StyledError from '../StyledError' import FlatPrimaryButton from '../FlatPrimaryButton' import NewMeetingActionsCurrentMeetings from '../NewMeetingActionsCurrentMeetings' import NewMeetingTeamPicker from '../NewMeetingTeamPicker' -import {AdhocTeamMultiSelect, Option} from '../AdhocTeamMultiSelect/AdhocTeamMultiSelect' -import {Select} from '../../ui/Select/Select' -import {SelectTrigger} from '../../ui/Select/SelectTrigger' -import {SelectValue} from '../../ui/Select/SelectValue' -import {SelectContent} from '../../ui/Select/SelectContent' -import {SelectGroup} from '../../ui/Select/SelectGroup' -import {SelectItem} from '../../ui/Select/SelectItem' -import OneOnOneTeamStatus from './OneOnOneTeamStatus' import ScheduleMeetingButton from './ScheduleMeetingButton' import useBreakpoint from '../../hooks/useBreakpoint' import {Breakpoint} from '../../types/constEnums' @@ -156,37 +147,6 @@ const ActivityDetailsSidebar = (props: Props) => { const mutationProps = useMutationProps() const {onError, onCompleted, submitting, submitMutation, error} = mutationProps const history = useHistory() - const {organizations: viewerOrganizations} = viewer - const [selectedUser, setSelectedUser] = React.useState