Merge branch 'main' into andrew/update-members-table-remove-from-project

This commit is contained in:
andrewwallacespeckle
2025-04-07 11:10:06 +01:00
88 changed files with 2463 additions and 1095 deletions
+2 -1
View File
@@ -38,7 +38,8 @@
"postinstall": "husky install",
"cm": "cz",
"eslint:inspect": "eslint-config-inspector",
"eslint:projectwide": "node ./utils/eslint-projectwide.mjs"
"eslint:projectwide": "node ./utils/eslint-projectwide.mjs",
"npkill": "npkill"
},
"devDependencies": {
"@eslint/config-inspector": "^0.4.10",
@@ -4012,8 +4012,11 @@ export type UserProjectCollection = {
export type UserProjectsFilter = {
/** Only include projects where user has the specified roles */
onlyWithRoles?: InputMaybe<Array<Scalars['String']['input']>>;
/** Only include personal projects (not in any workspace) */
personalOnly?: InputMaybe<Scalars['Boolean']['input']>;
/** Filter out projects by name */
search?: InputMaybe<Scalars['String']['input']>;
/** Only include projects in the specified workspace */
workspaceId?: InputMaybe<Scalars['ID']['input']>;
};
@@ -4698,6 +4701,7 @@ export type WorkspacePlan = {
name: WorkspacePlans;
paymentMethod: WorkspacePaymentMethod;
status: WorkspacePlanStatuses;
usage: WorkspacePlanUsage;
};
export type WorkspacePlanPrice = {
@@ -4716,6 +4720,12 @@ export enum WorkspacePlanStatuses {
Valid = 'valid'
}
export type WorkspacePlanUsage = {
__typename?: 'WorkspacePlanUsage';
modelCount: Scalars['Int']['output'];
projectCount: Scalars['Int']['output'];
};
export enum WorkspacePlans {
Academia = 'academia',
Business = 'business',
@@ -4789,6 +4799,8 @@ export type WorkspaceProjectMutationsUpdateRoleArgs = {
export type WorkspaceProjectsFilter = {
/** Filter out projects by name */
search?: InputMaybe<Scalars['String']['input']>;
/** Only return workspace projects that the active user has an explicit project role in */
withProjectRoleOnly?: InputMaybe<Scalars['Boolean']['input']>;
};
export type WorkspaceProjectsUpdatedMessage = {
+2 -2
View File
@@ -43,11 +43,11 @@
"vue-tippy": "^6.2.0"
},
"devDependencies": {
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/cli": "^5.0.5",
"@graphql-codegen/client-preset": "^4.3.0",
"@nuxt/eslint": "^0.3.13",
"@nuxtjs/tailwindcss": "^6.7.0",
"@parcel/watcher": "^2.4.1",
"@parcel/watcher": "^2.5.1",
"@types/apollo-upload-client": "^17.0.1",
"@types/eslint": "^8.56.10",
"@types/lodash-es": "^4.17.6",
@@ -42,11 +42,8 @@ import { workspaceRoute } from '~/lib/common/helpers/route'
graphql(`
fragment ProjectPageProjectHeader on Project {
id
role
name
description
visibility
allowPublicComments
workspace {
id
slug
@@ -62,7 +62,6 @@ import { graphql } from '~~/lib/common/generated/gql'
graphql(`
fragment ProjectPageSettingsGeneralBlockProjectInfo_Project on Project {
id
role
name
description
}
@@ -1,55 +0,0 @@
<template>
<LayoutDialog v-model:open="isOpen" max-width="sm">
<template #header>Manage Project</template>
<div class="flex flex-col text-foreground"></div>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { ProjectPageTeamDialogFragment } from '~~/lib/common/generated/gql/graphql'
import { graphql } from '~~/lib/common/generated/gql'
import type { OpenSectionType } from '~~/lib/projects/helpers/components'
graphql(`
fragment ProjectPageTeamDialog on Project {
id
name
role
allowPublicComments
visibility
team {
id
role
user {
...LimitedUserAvatar
role
}
}
invitedTeam {
id
title
inviteId
role
user {
...LimitedUserAvatar
role
}
}
...ProjectsPageTeamDialogManagePermissions_Project
}
`)
const emit = defineEmits<{
(e: 'update:open', v: boolean): void
}>()
const props = defineProps<{
open: boolean
project: ProjectPageTeamDialogFragment
openSection?: OpenSectionType
}>()
const isOpen = computed({
get: () => props.open,
set: (newVal) => emit('update:open', newVal)
})
</script>
@@ -103,7 +103,7 @@ const {
filter: {
search: (search.value || '').trim() || null,
onlyWithRoles: selectedRoles.value?.length ? selectedRoles.value : null,
workspaceId: isWorkspaceNewPlansEnabled ? (null as Nullable<string>) : undefined
personalOnly: isWorkspaceNewPlansEnabled.value
},
cursor: null as Nullable<string>
}))
@@ -110,7 +110,6 @@ graphql(`
cursor
items {
...ProjectsMoveToWorkspaceDialog_Project
role
workspace {
id
}
@@ -29,10 +29,16 @@
</div>
</div>
<div class="flex flex-col gap-2 w-full">
<FormCheckbox
v-model="enableDomainDiscoverabilityModel"
name="enableDomainDiscoverability"
:label="`Allow users with the @${verifiedDomain} domain to request to join workspace`"
/>
</div>
<div class="flex flex-col gap-3 mt-4 w-full md:max-w-96">
<FormButton size="lg" submit full-width>
{{ nextButtonText }}
</FormButton>
<FormButton size="lg" submit full-width>{{ nextButtonText }}</FormButton>
<FormButton color="subtle" size="lg" full-width @click.stop="goToPreviousStep">
Back
</FormButton>
@@ -47,11 +53,14 @@ import { PlusIcon } from '@heroicons/vue/24/outline'
import { isEmailOrEmpty } from '~~/lib/common/helpers/validation'
import { useForm, useFieldArray } from 'vee-validate'
import { useMixpanel } from '~/lib/core/composables/mp'
import { useVerifiedUserEmailDomains } from '~/lib/workspaces/composables/security'
import { isUndefined } from 'lodash-es'
interface InviteForm {
fields: string[]
}
const { domains } = useVerifiedUserEmailDomains()
const { goToNextStep, goToPreviousStep, state } = useWorkspacesWizard()
const mixpanel = useMixpanel()
const { handleSubmit } = useForm<InviteForm>({
@@ -61,10 +70,26 @@ const { handleSubmit } = useForm<InviteForm>({
})
const { fields, push } = useFieldArray<string>('fields')
const nextButtonText = computed(() =>
fields.value.filter((field) => !!field.value).length > 0 ? 'Continue' : 'Skip'
const enableDomainDiscoverabilityModel = ref<true | undefined>(
!isUndefined(state.value.enableDomainDiscoverabilityForDomain)
? state.value.enableDomainDiscoverabilityForDomain !== null
? true
: undefined
: true
)
const nextButtonText = computed(() =>
fields.value.filter((field) => !!field.value).length > 0
? 'Continue'
: 'Continue without inviting'
)
const verifiedDomain = computed(() => {
// only support enabling domain discoverability if there's one verified unblocked domain
if (domains.value.length !== 1) return undefined
return domains.value[0]
})
const onAddInvite = () => {
push('')
}
@@ -76,6 +101,12 @@ const onSubmit = handleSubmit(() => {
state.value.invites = validInvites
if (enableDomainDiscoverabilityModel.value && verifiedDomain.value) {
state.value.enableDomainDiscoverabilityForDomain = verifiedDomain.value
} else {
state.value.enableDomainDiscoverabilityForDomain = null
}
mixpanel.track('Workspace Invites Step Completed', {
inviteCount: validInvites
})
@@ -13,6 +13,7 @@ export const activeUserQuery = graphql(`
email
emails {
id
email
verified
}
company
@@ -65,7 +65,7 @@ type Documents = {
"\n fragment ProjectModelPageDialogMoveToVersion on Version {\n id\n message\n }\n": typeof types.ProjectModelPageDialogMoveToVersionFragmentDoc,
"\n fragment ProjectsModelPageEmbed_Project on Project {\n id\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n": typeof types.ProjectsModelPageEmbed_ProjectFragmentDoc,
"\n fragment ProjectModelPageVersionsCardVersion on Version {\n id\n message\n authorUser {\n ...LimitedUserAvatar\n }\n createdAt\n previewUrl\n sourceApplication\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectModelPageDialogDeleteVersion\n ...ProjectModelPageDialogMoveToVersion\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n }\n": typeof types.ProjectModelPageVersionsCardVersionFragmentDoc,
"\n fragment ProjectPageProjectHeader on Project {\n id\n role\n name\n description\n visibility\n allowPublicComments\n workspace {\n id\n slug\n name\n logo\n }\n }\n": typeof types.ProjectPageProjectHeaderFragmentDoc,
"\n fragment ProjectPageProjectHeader on Project {\n id\n name\n description\n workspace {\n id\n slug\n name\n logo\n }\n }\n": typeof types.ProjectPageProjectHeaderFragmentDoc,
"\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction on AutomationRevisionFunction {\n parameters\n release {\n id\n versionTag\n createdAt\n inputSchema\n function {\n id\n }\n }\n }\n": typeof types.ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunctionFragmentDoc,
"\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevision on AutomationRevision {\n id\n triggerDefinitions {\n ... on VersionCreatedTriggerDefinition {\n type\n model {\n id\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": typeof types.ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFragmentDoc,
"\n fragment ProjectPageAutomationFunctions_Automation on Automation {\n id\n currentRevision {\n id\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevision\n functions {\n release {\n id\n inputSchema\n function {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n releases(limit: 1) {\n items {\n id\n }\n }\n }\n }\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction\n }\n }\n }\n": typeof types.ProjectPageAutomationFunctions_AutomationFragmentDoc,
@@ -91,8 +91,7 @@ type Documents = {
"\n fragment ProjectPageSettingsGeneralBlockDelete_Project on Project {\n ...ProjectsDeleteDialog_Project\n }\n": typeof types.ProjectPageSettingsGeneralBlockDelete_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsGeneralBlockDiscussions_Project on Project {\n id\n visibility\n allowPublicComments\n }\n": typeof types.ProjectPageSettingsGeneralBlockDiscussions_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsGeneralBlockLeave_Project on Project {\n id\n name\n role\n team {\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n workspace {\n id\n }\n }\n": typeof types.ProjectPageSettingsGeneralBlockLeave_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsGeneralBlockProjectInfo_Project on Project {\n id\n role\n name\n description\n }\n": typeof types.ProjectPageSettingsGeneralBlockProjectInfo_ProjectFragmentDoc,
"\n fragment ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n id\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n": typeof types.ProjectPageTeamDialogFragmentDoc,
"\n fragment ProjectPageSettingsGeneralBlockProjectInfo_Project on Project {\n id\n name\n description\n }\n": typeof types.ProjectPageSettingsGeneralBlockProjectInfo_ProjectFragmentDoc,
"\n fragment ProjectsPageTeamDialogManagePermissions_Project on Project {\n id\n visibility\n role\n }\n": typeof types.ProjectsPageTeamDialogManagePermissions_ProjectFragmentDoc,
"\n fragment ProjectsDashboard_UserProjectCollection on UserProjectCollection {\n numberOfHidden\n }\n": typeof types.ProjectsDashboard_UserProjectCollectionFragmentDoc,
"\n fragment ProjectsDashboardFilledProject on ProjectCollection {\n items {\n ...ProjectDashboardItem\n }\n }\n": typeof types.ProjectsDashboardFilledProjectFragmentDoc,
@@ -135,7 +134,7 @@ type Documents = {
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": typeof types.ViewerCommentsListItemFragmentDoc,
"\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n": typeof types.ViewerModelVersionCardItemFragmentDoc,
"\n fragment MoveProjectsDialog_Workspace on Workspace {\n id\n ...ProjectsMoveToWorkspaceDialog_Workspace\n projects {\n items {\n id\n }\n }\n }\n": typeof types.MoveProjectsDialog_WorkspaceFragmentDoc,
"\n fragment MoveProjectsDialog_User on User {\n projects(cursor: $cursor, filter: $filter, limit: 10) {\n totalCount\n cursor\n items {\n ...ProjectsMoveToWorkspaceDialog_Project\n role\n workspace {\n id\n }\n }\n }\n }\n": typeof types.MoveProjectsDialog_UserFragmentDoc,
"\n fragment MoveProjectsDialog_User on User {\n projects(cursor: $cursor, filter: $filter, limit: 10) {\n totalCount\n cursor\n items {\n ...ProjectsMoveToWorkspaceDialog_Project\n workspace {\n id\n }\n }\n }\n }\n": typeof types.MoveProjectsDialog_UserFragmentDoc,
"\n fragment WorkspaceProjectList_Workspace on Workspace {\n id\n ...WorkspaceBase_Workspace\n ...WorkspaceTeam_Workspace\n ...WorkspaceSecurity_Workspace\n ...BillingAlert_Workspace\n ...InviteDialogWorkspace_Workspace\n projects {\n ...WorkspaceProjectList_ProjectCollection\n }\n creationState {\n completed\n state\n }\n readOnly\n }\n": typeof types.WorkspaceProjectList_WorkspaceFragmentDoc,
"\n fragment WorkspaceProjectList_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...ProjectDashboardItem\n }\n cursor\n }\n": typeof types.WorkspaceProjectList_ProjectCollectionFragmentDoc,
"\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceBase_Workspace\n ...WorkspaceTeam_Workspace\n ...BillingAlert_Workspace\n slug\n readOnly\n }\n": typeof types.WorkspaceHeader_WorkspaceFragmentDoc,
@@ -147,7 +146,7 @@ type Documents = {
"\n fragment WorkspaceSidebar_Workspace on Workspace {\n ...WorkspaceDashboardAbout_Workspace\n ...WorkspaceTeam_Workspace\n ...WorkspaceSecurity_Workspace\n slug\n plan {\n status\n }\n }\n": typeof types.WorkspaceSidebar_WorkspaceFragmentDoc,
"\n fragment WorkspaceWizard_Workspace on Workspace {\n creationState {\n completed\n state\n }\n name\n slug\n }\n": typeof types.WorkspaceWizard_WorkspaceFragmentDoc,
"\n fragment WorkspaceWizardStepRegion_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": typeof types.WorkspaceWizardStepRegion_ServerInfoFragmentDoc,
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n verified\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n": typeof types.ActiveUserMainMetadataDocument,
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n": typeof types.ActiveUserMainMetadataDocument,
"\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n ": typeof types.CreateOnboardingProjectDocument,
"\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n }\n": typeof types.FullPermissionCheckResultFragmentDoc,
"\n mutation FinishOnboarding($input: OnboardingCompletionInput) {\n activeUserMutations {\n finishOnboarding(input: $input)\n }\n }\n": typeof types.FinishOnboardingDocument,
@@ -209,6 +208,7 @@ type Documents = {
"\n query NavigationInvites {\n activeUser {\n id\n projectInvites {\n ...HeaderNavNotificationsProjectInvite_PendingStreamCollaborator\n }\n workspaceInvites {\n ...HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator\n }\n }\n }\n": typeof types.NavigationInvitesDocument,
"\n fragment ProjectPageTeamInternals_Project on Project {\n id\n role\n invitedTeam {\n id\n title\n role\n inviteId\n user {\n role\n ...LimitedUserAvatar\n }\n }\n team {\n role\n seatType\n user {\n id\n role\n ...LimitedUserAvatar\n }\n }\n }\n": typeof types.ProjectPageTeamInternals_ProjectFragmentDoc,
"\n fragment ProjectPageTeamInternals_Workspace on Workspace {\n id\n team {\n items {\n id\n role\n user {\n id\n }\n }\n }\n }\n": typeof types.ProjectPageTeamInternals_WorkspaceFragmentDoc,
"\n fragment ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n id\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n": typeof types.ProjectPageTeamDialogFragmentDoc,
"\n fragment ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n id\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\n }\n": typeof types.ProjectDashboardItemNoModelsFragmentDoc,
"\n fragment ProjectDashboardItem on Project {\n id\n ...ProjectDashboardItemNoModels\n models(limit: 4) {\n totalCount\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n workspace {\n id\n slug\n name\n logo\n readOnly\n }\n pendingImportedModels(limit: 4) {\n ...PendingFileUpload\n }\n }\n": typeof types.ProjectDashboardItemFragmentDoc,
"\n fragment PendingFileUpload on FileUpload {\n id\n projectId\n modelName\n convertedStatus\n convertedMessage\n uploadDate\n convertedLastUpdate\n fileType\n fileName\n }\n": typeof types.PendingFileUploadFragmentDoc,
@@ -315,7 +315,7 @@ type Documents = {
"\n query SettingsWorkspacesMembersSearch($slug: String!, $filter: WorkspaceTeamFilter) {\n workspaceBySlug(slug: $slug) {\n id\n team(filter: $filter, limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersTable_WorkspaceCollaborator\n }\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersSearchDocument,
"\n query SettingsWorkspacesInvitesSearch(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n": typeof types.SettingsWorkspacesInvitesSearchDocument,
"\n query SettingsWorkspacesProjects(\n $slug: String!\n $limit: Int!\n $cursor: String\n $filter: WorkspaceProjectsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n id\n slug\n readOnly\n projects(limit: $limit, cursor: $cursor, filter: $filter) {\n cursor\n ...SettingsWorkspacesProjects_ProjectCollection\n }\n }\n }\n": typeof types.SettingsWorkspacesProjectsDocument,
"\n query SettingsWorkspaceSecurity($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesSecurity_Workspace\n }\n activeUser {\n ...SettingsWorkspacesSecurity_User\n }\n }\n": typeof types.SettingsWorkspaceSecurityDocument,
"\n query SettingsWorkspaceSecurity($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesSecurity_Workspace\n }\n }\n": typeof types.SettingsWorkspaceSecurityDocument,
"\n fragment AppAuthorAvatar on AppAuthor {\n id\n name\n avatar\n }\n": typeof types.AppAuthorAvatarFragmentDoc,
"\n fragment LimitedUserAvatar on LimitedUser {\n id\n name\n avatar\n }\n": typeof types.LimitedUserAvatarFragmentDoc,
"\n fragment ActiveUserAvatar on User {\n id\n name\n avatar\n }\n": typeof types.ActiveUserAvatarFragmentDoc,
@@ -411,7 +411,7 @@ type Documents = {
"\n fragment SettingsWorkspacesProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": typeof types.SettingsWorkspacesProjects_ProjectCollectionFragmentDoc,
"\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n role\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n hasAccessToMultiRegion: hasAccessToFeature(\n featureName: workspaceDataRegionSpecificity\n )\n hasProjects: projects(limit: 0) {\n totalCount\n }\n }\n": typeof types.SettingsWorkspacesRegions_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesRegions_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": typeof types.SettingsWorkspacesRegions_ServerInfoFragmentDoc,
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n\n fragment SettingsWorkspacesSecurity_User on User {\n id\n emails {\n id\n email\n verified\n }\n }\n": typeof types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n": typeof types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
};
const documents: Documents = {
"\n fragment AuthLoginWithEmailBlock_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n email\n user {\n id\n }\n }\n": types.AuthLoginWithEmailBlock_PendingWorkspaceCollaboratorFragmentDoc,
@@ -465,7 +465,7 @@ const documents: Documents = {
"\n fragment ProjectModelPageDialogMoveToVersion on Version {\n id\n message\n }\n": types.ProjectModelPageDialogMoveToVersionFragmentDoc,
"\n fragment ProjectsModelPageEmbed_Project on Project {\n id\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n": types.ProjectsModelPageEmbed_ProjectFragmentDoc,
"\n fragment ProjectModelPageVersionsCardVersion on Version {\n id\n message\n authorUser {\n ...LimitedUserAvatar\n }\n createdAt\n previewUrl\n sourceApplication\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectModelPageDialogDeleteVersion\n ...ProjectModelPageDialogMoveToVersion\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n }\n": types.ProjectModelPageVersionsCardVersionFragmentDoc,
"\n fragment ProjectPageProjectHeader on Project {\n id\n role\n name\n description\n visibility\n allowPublicComments\n workspace {\n id\n slug\n name\n logo\n }\n }\n": types.ProjectPageProjectHeaderFragmentDoc,
"\n fragment ProjectPageProjectHeader on Project {\n id\n name\n description\n workspace {\n id\n slug\n name\n logo\n }\n }\n": types.ProjectPageProjectHeaderFragmentDoc,
"\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction on AutomationRevisionFunction {\n parameters\n release {\n id\n versionTag\n createdAt\n inputSchema\n function {\n id\n }\n }\n }\n": types.ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunctionFragmentDoc,
"\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevision on AutomationRevision {\n id\n triggerDefinitions {\n ... on VersionCreatedTriggerDefinition {\n type\n model {\n id\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFragmentDoc,
"\n fragment ProjectPageAutomationFunctions_Automation on Automation {\n id\n currentRevision {\n id\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevision\n functions {\n release {\n id\n inputSchema\n function {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n releases(limit: 1) {\n items {\n id\n }\n }\n }\n }\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction\n }\n }\n }\n": types.ProjectPageAutomationFunctions_AutomationFragmentDoc,
@@ -491,8 +491,7 @@ const documents: Documents = {
"\n fragment ProjectPageSettingsGeneralBlockDelete_Project on Project {\n ...ProjectsDeleteDialog_Project\n }\n": types.ProjectPageSettingsGeneralBlockDelete_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsGeneralBlockDiscussions_Project on Project {\n id\n visibility\n allowPublicComments\n }\n": types.ProjectPageSettingsGeneralBlockDiscussions_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsGeneralBlockLeave_Project on Project {\n id\n name\n role\n team {\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n workspace {\n id\n }\n }\n": types.ProjectPageSettingsGeneralBlockLeave_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsGeneralBlockProjectInfo_Project on Project {\n id\n role\n name\n description\n }\n": types.ProjectPageSettingsGeneralBlockProjectInfo_ProjectFragmentDoc,
"\n fragment ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n id\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n": types.ProjectPageTeamDialogFragmentDoc,
"\n fragment ProjectPageSettingsGeneralBlockProjectInfo_Project on Project {\n id\n name\n description\n }\n": types.ProjectPageSettingsGeneralBlockProjectInfo_ProjectFragmentDoc,
"\n fragment ProjectsPageTeamDialogManagePermissions_Project on Project {\n id\n visibility\n role\n }\n": types.ProjectsPageTeamDialogManagePermissions_ProjectFragmentDoc,
"\n fragment ProjectsDashboard_UserProjectCollection on UserProjectCollection {\n numberOfHidden\n }\n": types.ProjectsDashboard_UserProjectCollectionFragmentDoc,
"\n fragment ProjectsDashboardFilledProject on ProjectCollection {\n items {\n ...ProjectDashboardItem\n }\n }\n": types.ProjectsDashboardFilledProjectFragmentDoc,
@@ -535,7 +534,7 @@ const documents: Documents = {
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": types.ViewerCommentsListItemFragmentDoc,
"\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n": types.ViewerModelVersionCardItemFragmentDoc,
"\n fragment MoveProjectsDialog_Workspace on Workspace {\n id\n ...ProjectsMoveToWorkspaceDialog_Workspace\n projects {\n items {\n id\n }\n }\n }\n": types.MoveProjectsDialog_WorkspaceFragmentDoc,
"\n fragment MoveProjectsDialog_User on User {\n projects(cursor: $cursor, filter: $filter, limit: 10) {\n totalCount\n cursor\n items {\n ...ProjectsMoveToWorkspaceDialog_Project\n role\n workspace {\n id\n }\n }\n }\n }\n": types.MoveProjectsDialog_UserFragmentDoc,
"\n fragment MoveProjectsDialog_User on User {\n projects(cursor: $cursor, filter: $filter, limit: 10) {\n totalCount\n cursor\n items {\n ...ProjectsMoveToWorkspaceDialog_Project\n workspace {\n id\n }\n }\n }\n }\n": types.MoveProjectsDialog_UserFragmentDoc,
"\n fragment WorkspaceProjectList_Workspace on Workspace {\n id\n ...WorkspaceBase_Workspace\n ...WorkspaceTeam_Workspace\n ...WorkspaceSecurity_Workspace\n ...BillingAlert_Workspace\n ...InviteDialogWorkspace_Workspace\n projects {\n ...WorkspaceProjectList_ProjectCollection\n }\n creationState {\n completed\n state\n }\n readOnly\n }\n": types.WorkspaceProjectList_WorkspaceFragmentDoc,
"\n fragment WorkspaceProjectList_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...ProjectDashboardItem\n }\n cursor\n }\n": types.WorkspaceProjectList_ProjectCollectionFragmentDoc,
"\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceBase_Workspace\n ...WorkspaceTeam_Workspace\n ...BillingAlert_Workspace\n slug\n readOnly\n }\n": types.WorkspaceHeader_WorkspaceFragmentDoc,
@@ -547,7 +546,7 @@ const documents: Documents = {
"\n fragment WorkspaceSidebar_Workspace on Workspace {\n ...WorkspaceDashboardAbout_Workspace\n ...WorkspaceTeam_Workspace\n ...WorkspaceSecurity_Workspace\n slug\n plan {\n status\n }\n }\n": types.WorkspaceSidebar_WorkspaceFragmentDoc,
"\n fragment WorkspaceWizard_Workspace on Workspace {\n creationState {\n completed\n state\n }\n name\n slug\n }\n": types.WorkspaceWizard_WorkspaceFragmentDoc,
"\n fragment WorkspaceWizardStepRegion_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": types.WorkspaceWizardStepRegion_ServerInfoFragmentDoc,
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n verified\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n": types.ActiveUserMainMetadataDocument,
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n": types.ActiveUserMainMetadataDocument,
"\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n ": types.CreateOnboardingProjectDocument,
"\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n }\n": types.FullPermissionCheckResultFragmentDoc,
"\n mutation FinishOnboarding($input: OnboardingCompletionInput) {\n activeUserMutations {\n finishOnboarding(input: $input)\n }\n }\n": types.FinishOnboardingDocument,
@@ -609,6 +608,7 @@ const documents: Documents = {
"\n query NavigationInvites {\n activeUser {\n id\n projectInvites {\n ...HeaderNavNotificationsProjectInvite_PendingStreamCollaborator\n }\n workspaceInvites {\n ...HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator\n }\n }\n }\n": types.NavigationInvitesDocument,
"\n fragment ProjectPageTeamInternals_Project on Project {\n id\n role\n invitedTeam {\n id\n title\n role\n inviteId\n user {\n role\n ...LimitedUserAvatar\n }\n }\n team {\n role\n seatType\n user {\n id\n role\n ...LimitedUserAvatar\n }\n }\n }\n": types.ProjectPageTeamInternals_ProjectFragmentDoc,
"\n fragment ProjectPageTeamInternals_Workspace on Workspace {\n id\n team {\n items {\n id\n role\n user {\n id\n }\n }\n }\n }\n": types.ProjectPageTeamInternals_WorkspaceFragmentDoc,
"\n fragment ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n id\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n": types.ProjectPageTeamDialogFragmentDoc,
"\n fragment ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n id\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\n }\n": types.ProjectDashboardItemNoModelsFragmentDoc,
"\n fragment ProjectDashboardItem on Project {\n id\n ...ProjectDashboardItemNoModels\n models(limit: 4) {\n totalCount\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n workspace {\n id\n slug\n name\n logo\n readOnly\n }\n pendingImportedModels(limit: 4) {\n ...PendingFileUpload\n }\n }\n": types.ProjectDashboardItemFragmentDoc,
"\n fragment PendingFileUpload on FileUpload {\n id\n projectId\n modelName\n convertedStatus\n convertedMessage\n uploadDate\n convertedLastUpdate\n fileType\n fileName\n }\n": types.PendingFileUploadFragmentDoc,
@@ -715,7 +715,7 @@ const documents: Documents = {
"\n query SettingsWorkspacesMembersSearch($slug: String!, $filter: WorkspaceTeamFilter) {\n workspaceBySlug(slug: $slug) {\n id\n team(filter: $filter, limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersTable_WorkspaceCollaborator\n }\n }\n }\n }\n": types.SettingsWorkspacesMembersSearchDocument,
"\n query SettingsWorkspacesInvitesSearch(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n": types.SettingsWorkspacesInvitesSearchDocument,
"\n query SettingsWorkspacesProjects(\n $slug: String!\n $limit: Int!\n $cursor: String\n $filter: WorkspaceProjectsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n id\n slug\n readOnly\n projects(limit: $limit, cursor: $cursor, filter: $filter) {\n cursor\n ...SettingsWorkspacesProjects_ProjectCollection\n }\n }\n }\n": types.SettingsWorkspacesProjectsDocument,
"\n query SettingsWorkspaceSecurity($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesSecurity_Workspace\n }\n activeUser {\n ...SettingsWorkspacesSecurity_User\n }\n }\n": types.SettingsWorkspaceSecurityDocument,
"\n query SettingsWorkspaceSecurity($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesSecurity_Workspace\n }\n }\n": types.SettingsWorkspaceSecurityDocument,
"\n fragment AppAuthorAvatar on AppAuthor {\n id\n name\n avatar\n }\n": types.AppAuthorAvatarFragmentDoc,
"\n fragment LimitedUserAvatar on LimitedUser {\n id\n name\n avatar\n }\n": types.LimitedUserAvatarFragmentDoc,
"\n fragment ActiveUserAvatar on User {\n id\n name\n avatar\n }\n": types.ActiveUserAvatarFragmentDoc,
@@ -811,7 +811,7 @@ const documents: Documents = {
"\n fragment SettingsWorkspacesProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsWorkspacesProjects_ProjectCollectionFragmentDoc,
"\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n role\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n hasAccessToMultiRegion: hasAccessToFeature(\n featureName: workspaceDataRegionSpecificity\n )\n hasProjects: projects(limit: 0) {\n totalCount\n }\n }\n": types.SettingsWorkspacesRegions_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesRegions_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": types.SettingsWorkspacesRegions_ServerInfoFragmentDoc,
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n\n fragment SettingsWorkspacesSecurity_User on User {\n id\n emails {\n id\n email\n verified\n }\n }\n": types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n": types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
};
/**
@@ -1035,7 +1035,7 @@ export function graphql(source: "\n fragment ProjectModelPageVersionsCardVersio
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageProjectHeader on Project {\n id\n role\n name\n description\n visibility\n allowPublicComments\n workspace {\n id\n slug\n name\n logo\n }\n }\n"): (typeof documents)["\n fragment ProjectPageProjectHeader on Project {\n id\n role\n name\n description\n visibility\n allowPublicComments\n workspace {\n id\n slug\n name\n logo\n }\n }\n"];
export function graphql(source: "\n fragment ProjectPageProjectHeader on Project {\n id\n name\n description\n workspace {\n id\n slug\n name\n logo\n }\n }\n"): (typeof documents)["\n fragment ProjectPageProjectHeader on Project {\n id\n name\n description\n workspace {\n id\n slug\n name\n logo\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1139,11 +1139,7 @@ export function graphql(source: "\n fragment ProjectPageSettingsGeneralBlockLea
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageSettingsGeneralBlockProjectInfo_Project on Project {\n id\n role\n name\n description\n }\n"): (typeof documents)["\n fragment ProjectPageSettingsGeneralBlockProjectInfo_Project on Project {\n id\n role\n name\n description\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n id\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n"): (typeof documents)["\n fragment ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n id\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n"];
export function graphql(source: "\n fragment ProjectPageSettingsGeneralBlockProjectInfo_Project on Project {\n id\n name\n description\n }\n"): (typeof documents)["\n fragment ProjectPageSettingsGeneralBlockProjectInfo_Project on Project {\n id\n name\n description\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1315,7 +1311,7 @@ export function graphql(source: "\n fragment MoveProjectsDialog_Workspace on Wo
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment MoveProjectsDialog_User on User {\n projects(cursor: $cursor, filter: $filter, limit: 10) {\n totalCount\n cursor\n items {\n ...ProjectsMoveToWorkspaceDialog_Project\n role\n workspace {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n fragment MoveProjectsDialog_User on User {\n projects(cursor: $cursor, filter: $filter, limit: 10) {\n totalCount\n cursor\n items {\n ...ProjectsMoveToWorkspaceDialog_Project\n role\n workspace {\n id\n }\n }\n }\n }\n"];
export function graphql(source: "\n fragment MoveProjectsDialog_User on User {\n projects(cursor: $cursor, filter: $filter, limit: 10) {\n totalCount\n cursor\n items {\n ...ProjectsMoveToWorkspaceDialog_Project\n workspace {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n fragment MoveProjectsDialog_User on User {\n projects(cursor: $cursor, filter: $filter, limit: 10) {\n totalCount\n cursor\n items {\n ...ProjectsMoveToWorkspaceDialog_Project\n workspace {\n id\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1363,7 +1359,7 @@ export function graphql(source: "\n fragment WorkspaceWizardStepRegion_ServerIn
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n verified\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n"): (typeof documents)["\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n verified\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n"];
export function graphql(source: "\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n"): (typeof documents)["\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1608,6 +1604,10 @@ export function graphql(source: "\n fragment ProjectPageTeamInternals_Project o
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageTeamInternals_Workspace on Workspace {\n id\n team {\n items {\n id\n role\n user {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageTeamInternals_Workspace on Workspace {\n id\n team {\n items {\n id\n role\n user {\n id\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n id\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n"): (typeof documents)["\n fragment ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n id\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2035,7 +2035,7 @@ export function graphql(source: "\n query SettingsWorkspacesProjects(\n $slu
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SettingsWorkspaceSecurity($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesSecurity_Workspace\n }\n activeUser {\n ...SettingsWorkspacesSecurity_User\n }\n }\n"): (typeof documents)["\n query SettingsWorkspaceSecurity($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesSecurity_Workspace\n }\n activeUser {\n ...SettingsWorkspacesSecurity_User\n }\n }\n"];
export function graphql(source: "\n query SettingsWorkspaceSecurity($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesSecurity_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspaceSecurity($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesSecurity_Workspace\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2419,7 +2419,7 @@ export function graphql(source: "\n fragment SettingsWorkspacesRegions_ServerIn
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n\n fragment SettingsWorkspacesSecurity_User on User {\n id\n emails {\n id\n email\n verified\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n\n fragment SettingsWorkspacesSecurity_User on User {\n id\n emails {\n id\n email\n verified\n }\n }\n"];
export function graphql(source: "\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};
File diff suppressed because one or more lines are too long
@@ -1,5 +1,34 @@
import { graphql } from '~~/lib/common/generated/gql'
export const projectPageTeamDialogFragment = graphql(`
fragment ProjectPageTeamDialog on Project {
id
name
role
allowPublicComments
visibility
team {
id
role
user {
...LimitedUserAvatar
role
}
}
invitedTeam {
id
title
inviteId
role
user {
...LimitedUserAvatar
role
}
}
...ProjectsPageTeamDialogManagePermissions_Project
}
`)
export const projectDashboardItemNoModelsFragment = graphql(`
fragment ProjectDashboardItemNoModels on Project {
id
@@ -126,8 +126,5 @@ export const settingsWorkspacesSecurityQuery = graphql(`
workspaceBySlug(slug: $slug) {
...SettingsWorkspacesSecurity_Workspace
}
activeUser {
...SettingsWorkspacesSecurity_User
}
}
`)
@@ -0,0 +1,24 @@
import { blockedDomains } from '@speckle/shared'
export const useVerifiedUserEmailDomains = (
options?: Partial<{
/**
* Whether to filter out blocked domains from the list
*/
filterBlocked: boolean
}>
) => {
const { filterBlocked = true } = options || {}
const { activeUser } = useActiveUser()
const domains = computed(() => {
return (activeUser.value?.emails || [])
.filter((email) => email.verified)
.map((email) => email.email.split('@')[1])
.filter(
(domain) => domain && (!filterBlocked || !blockedDomains.includes(domain))
)
})
return { domains }
}
@@ -28,7 +28,8 @@ const emptyState: WorkspaceWizardState = {
plan: null,
billingInterval: BillingInterval.Monthly,
id: '',
region: null
region: null,
enableDomainDiscoverabilityForDomain: undefined
}
const steps: readonly WizardSteps[] = [
@@ -129,7 +130,9 @@ export const useWorkspacesWizard = () => {
const newWorkspaceResult = await createWorkspace(
{
name: wizardState.value.state.name,
slug: wizardState.value.state.slug
slug: wizardState.value.state.slug,
enableDomainDiscoverabilityForDomain:
wizardState.value.state.enableDomainDiscoverabilityForDomain || null
},
{ navigateOnSuccess: false, hideNotifications: true }
)
@@ -1,3 +1,4 @@
import type { MaybeNullOrUndefined } from '@speckle/shared'
import type {
BillingInterval,
WorkspacePlans,
@@ -38,6 +39,7 @@ export type WorkspaceWizardState = {
billingInterval: BillingInterval | null
id: string
region: SettingsWorkspacesRegionsSelect_ServerRegionItemFragment | null
enableDomainDiscoverabilityForDomain: MaybeNullOrUndefined<string>
}
export enum WizardSteps {
+1 -1
View File
@@ -96,7 +96,7 @@
"@nuxt/eslint": "^1.1.0",
"@nuxt/image": "^1.8.1",
"@nuxtjs/tailwindcss": "^6.12.2",
"@parcel/watcher": "^2.4.1",
"@parcel/watcher": "^2.5.1",
"@speckle/tailwind-theme": "workspace:^",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.2",
@@ -132,6 +132,7 @@ import {
workspaceUpdateDomainProtectionMutation,
workspaceUpdateDiscoverabilityMutation
} from '~/lib/workspaces/graphql/mutations'
import { useVerifiedUserEmailDomains } from '~/lib/workspaces/composables/security'
graphql(`
fragment SettingsWorkspacesSecurity_Workspace on Workspace {
@@ -146,15 +147,6 @@ graphql(`
domainBasedMembershipProtectionEnabled
discoverabilityEnabled
}
fragment SettingsWorkspacesSecurity_User on User {
id
emails {
id
email
verified
}
}
`)
definePageMeta({
@@ -167,6 +159,9 @@ useHead({
const slug = computed(() => (route.params.slug as string) || '')
const { domains: userEmailDomains } = useVerifiedUserEmailDomains({
filterBlocked: false
})
const route = useRoute()
const addWorkspaceDomain = useAddWorkspaceDomain()
const isSsoEnabled = useIsWorkspacesSsoEnabled()
@@ -198,10 +193,7 @@ const verifiedUserDomains = computed(() => {
return [
...new Set(
(result.value?.activeUser?.emails ?? [])
.filter((email) => email.verified)
.map((email) => email.email.split('@')[1])
.filter((domain) => !workspaceDomainSet.has(domain))
userEmailDomains.value.filter((domain) => !workspaceDomainSet.has(domain))
)
]
})
@@ -208,7 +208,15 @@ input UserProjectsFilter {
"""
onlyWithRoles: [String!]
"""
Only include projects in the specified workspace
"""
workspaceId: ID
"""
Only include personal projects (not in any workspace)
"""
personalOnly: Boolean
}
enum UserProjectsUpdatedMessageType {
@@ -45,6 +45,10 @@ input WorkspaceCreateInput {
Logo image as base64-encoded string
"""
logo: String
"""
Add this domain to the workspace as a verified domain and enable domain discoverability
"""
enableDomainDiscoverabilityForDomain: String
}
input WorkspaceUpdateInput {
@@ -138,12 +142,8 @@ type WorkspaceMutations {
@hasServerRole(role: SERVER_USER)
leave(id: ID!): Boolean! @hasServerRole(role: SERVER_GUEST)
join(input: JoinWorkspaceInput!): Workspace! @hasScope(scope: "workspace:update")
# TODO: this mutation should have an hasWorkspaceRole directive to authorize only workspace admin
# We are, for the moment, doing the check in the resolver
addDomain(input: AddDomainToWorkspaceInput!): Workspace!
@hasScope(scope: "workspace:update")
# TODO: this mutation should have an hasWorkspaceRole directive to authorize only workspace admin
# We are, for the moment, doing the check in the resolver
deleteDomain(input: WorkspaceDomainDeleteInput!): Workspace!
@hasScope(scope: "workspace:update")
deleteSsoProvider(workspaceId: String!): Boolean!
@@ -406,6 +406,11 @@ input WorkspaceProjectsFilter {
Filter out projects by name
"""
search: String
"""
Only return workspace projects that the active user has an explicit project role in
"""
withProjectRoleOnly: Boolean
}
input WorkspaceTeamFilter {
@@ -550,6 +555,15 @@ extend type User {
extend type Project {
workspace: Workspace
"""
Returns information about the potential effects of moving a project to a given workspace.
"""
moveToWorkspaceDryRun(workspaceId: String!): ProjectMoveToWorkspaceDryRun!
}
type ProjectMoveToWorkspaceDryRun {
addedToWorkspace(limit: Int): [LimitedUser!]!
addedToWorkspaceTotalCount: Int!
}
type ServerWorkspacesInfo {
+1
View File
@@ -50,6 +50,7 @@ const startDebugger = process.env.START_DEBUGGER
if ((isTestEnv() || isDevEnv()) && startDebugger) {
const inspector = require('node:inspector')
if (!inspector.url()) {
console.log('Debugger starting on process ' + process.pid)
inspector.open(0, undefined, true)
}
}
+1
View File
@@ -72,6 +72,7 @@ generates:
WorkspaceSubscriptionSeats: '@/modules/gatekeeper/helpers/graphTypes#WorkspaceSubscriptionSeatsGraphQLReturn'
WorkspaceJoinRequest: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceJoinRequestGraphQLReturn'
LimitedWorkspaceJoinRequest: '@/modules/workspacesCore/helpers/graphTypes#LimitedWorkspaceJoinRequestGraphQLReturn'
ProjectMoveToWorkspaceDryRun: '@/modules/workspacesCore/helpers/graphTypes#ProjectMoveToWorkspaceDryRunGraphQLReturn'
Webhook: '@/modules/webhooks/helpers/graphTypes#WebhookGraphQLReturn'
SmartTextEditorValue: '@/modules/core/services/richTextEditorService#SmartTextEditorValueGraphQLReturn'
BlobMetadata: '@/modules/blobstorage/domain/types#BlobStorageItem'
+6 -1
View File
@@ -54,7 +54,12 @@ const configs = [
'@typescript-eslint/unbound-method': 'off', // too many false positives
'@typescript-eslint/no-unnecessary-type-assertion': 'off', // false positives - sometimes they are actually necessary
'@typescript-eslint/no-empty-object-type': 'off', // too restrictive
'@typescript-eslint/only-throw-error': ['error', { allow: ['AssertionError'] }],
'@typescript-eslint/only-throw-error': [
'error',
{
allow: ['AssertionError']
}
],
'@typescript-eslint/no-unused-vars': [
'error',
{
@@ -6,6 +6,7 @@ import cryptoRandomString from 'crypto-random-string'
import Redis from 'ioredis'
import { get, has, isObjectLike } from 'lodash'
import { Logger } from 'pino'
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
export enum AuthCodePayloadAction {
CreateAutomation = 'createAutomation',
@@ -87,7 +88,7 @@ export const validateStoredAuthCodeFactory =
// Token is valid, confirm user is authorized to access specified resources.
if (resources?.workspaceId) {
await emit({
eventName: 'workspace.authorized',
eventName: WorkspaceEvents.Authorizing,
payload: { userId: payload.userId, workspaceId: resources?.workspaceId }
})
}
@@ -1,6 +1,9 @@
import { defineModuleLoaders } from '@/modules/loaders'
import { getStreamFactory } from '@/modules/core/repositories/streams'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import {
adminOverrideEnabled,
getFeatureFlags
} from '@/modules/shared/helpers/envHelper'
import { db } from '@/db/knex'
// TODO: Move everything to use dataLoaders
@@ -8,6 +11,7 @@ export default defineModuleLoaders(async () => {
const getStream = getStreamFactory({ db })
return {
getAdminOverrideEnabled: async () => adminOverrideEnabled(),
getEnv: async () => getFeatureFlags(),
getProject: async ({ projectId }, { dataLoaders }) => {
return await dataLoaders.streams.getStream.load(projectId)
@@ -1,14 +1,9 @@
import { ProjectTeamMember } from '@/modules/core/domain/projects/types'
import { Project } from '@/modules/core/domain/streams/types'
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
import { MaybeNullOrUndefined, StreamRoles } from '@speckle/shared'
export type GetProject = (args: { projectId: string }) => Promise<Project | null>
export type GetProjectCollaborators = (args: {
projectId: string
}) => Promise<ProjectTeamMember[]>
export type UpdateProject = (args: {
projectUpdate: Pick<StreamRecord, 'id' | 'workspaceId'>
}) => Promise<StreamRecord>
@@ -178,7 +178,15 @@ export type BaseUserStreamsQueryParams = {
* Only allow streams with the specified IDs to be returned
*/
streamIdWhitelist?: string[]
workspaceId?: string | null
/**
* Only allow streams in the specified workspace to be returned
*/
workspaceId?: MaybeNullOrUndefined<string>
/**
* Only allow personal (non-workspace) streams to be returned
*/
personalOnly?: MaybeNullOrUndefined<boolean>
/**
* Only with active sso session
@@ -123,6 +123,7 @@ export type CreateValidatedUser = (
},
options?: Partial<{
skipPropertyValidation: boolean
allowPersonalEmail: boolean
}>
) => Promise<string>
@@ -12,29 +12,13 @@ import {
import { GraphqlDirectiveBuilder } from '@/modules/core/graph/helpers/directiveHelper'
import { getRolesFactory } from '@/modules/shared/repositories/roles'
import { db } from '@/db/knex'
import { authorizeResolverFactory } from '@/modules/shared/services/auth'
import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper'
import {
getUserAclRoleFactory,
getUserServerRoleFactory
} from '@/modules/shared/repositories/acl'
import { getStreamFactory } from '@/modules/core/repositories/streams'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { authorizeResolver } from '@/modules/shared'
const getStream = getStreamFactory({ db })
const throwForNotHavingServerRole = throwForNotHavingServerRoleFactory({
validateServerRole: validateServerRoleBuilderFactory({
getRoles: getRolesFactory({ db })
})
})
const authorizeResolver = authorizeResolverFactory({
getRoles: getRolesFactory({ db }),
adminOverrideEnabled,
getUserServerRole: getUserServerRoleFactory({ db }),
getStream,
getUserAclRole: getUserAclRoleFactory({ db }),
emitWorkspaceEvent: getEventBus().emit
})
/**
* Ensure that the user has the specified SERVER role (e.g. server user, admin etc.)
@@ -5,7 +5,7 @@ import { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn } from
import { PendingStreamCollaboratorGraphQLReturn } from '@/modules/serverinvites/helpers/graphTypes';
import { FileUploadGraphQLReturn } from '@/modules/fileuploads/helpers/types';
import { AutomateFunctionGraphQLReturn, AutomateFunctionReleaseGraphQLReturn, AutomationGraphQLReturn, AutomationRevisionGraphQLReturn, AutomationRevisionFunctionGraphQLReturn, AutomateRunGraphQLReturn, AutomationRunTriggerGraphQLReturn, AutomationRevisionTriggerDefinitionGraphQLReturn, AutomateFunctionRunGraphQLReturn, TriggeredAutomationsStatusGraphQLReturn, ProjectAutomationMutationsGraphQLReturn, ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn, ProjectAutomationsUpdatedMessageGraphQLReturn, UserAutomateInfoGraphQLReturn } from '@/modules/automate/helpers/graphTypes';
import { WorkspaceGraphQLReturn, WorkspaceSsoGraphQLReturn, WorkspaceMutationsGraphQLReturn, WorkspaceJoinRequestMutationsGraphQLReturn, WorkspaceInviteMutationsGraphQLReturn, WorkspaceProjectMutationsGraphQLReturn, PendingWorkspaceCollaboratorGraphQLReturn, WorkspaceCollaboratorGraphQLReturn, WorkspaceJoinRequestGraphQLReturn, LimitedWorkspaceJoinRequestGraphQLReturn, ProjectRoleGraphQLReturn, WorkspacePermissionChecksGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes';
import { WorkspaceGraphQLReturn, WorkspaceSsoGraphQLReturn, WorkspaceMutationsGraphQLReturn, WorkspaceJoinRequestMutationsGraphQLReturn, WorkspaceInviteMutationsGraphQLReturn, WorkspaceProjectMutationsGraphQLReturn, PendingWorkspaceCollaboratorGraphQLReturn, WorkspaceCollaboratorGraphQLReturn, WorkspaceJoinRequestGraphQLReturn, LimitedWorkspaceJoinRequestGraphQLReturn, ProjectMoveToWorkspaceDryRunGraphQLReturn, ProjectRoleGraphQLReturn, WorkspacePermissionChecksGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes';
import { WorkspacePlanGraphQLReturn, WorkspacePlanUsageGraphQLReturn, PriceGraphQLReturn } from '@/modules/gatekeeperCore/helpers/graphTypes';
import { WorkspaceBillingMutationsGraphQLReturn, WorkspaceSubscriptionSeatsGraphQLReturn, WorkspaceSubscriptionGraphQLReturn } from '@/modules/gatekeeper/helpers/graphTypes';
import { WebhookGraphQLReturn } from '@/modules/webhooks/helpers/graphTypes';
@@ -2031,6 +2031,8 @@ export type Project = {
* real or fake (e.g., with a foo/bar model, it will be nested under foo even if such a model doesn't actually exist)
*/
modelsTree: ModelsTreeItemCollection;
/** Returns information about the potential effects of moving a project to a given workspace. */
moveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRun;
name: Scalars['String']['output'];
object?: Maybe<Object>;
/** Pending project access requests */
@@ -2129,6 +2131,11 @@ export type ProjectModelsTreeArgs = {
};
export type ProjectMoveToWorkspaceDryRunArgs = {
workspaceId: Scalars['String']['input'];
};
export type ProjectObjectArgs = {
id: Scalars['String']['input'];
};
@@ -2449,6 +2456,17 @@ export const ProjectModelsUpdatedMessageType = {
} as const;
export type ProjectModelsUpdatedMessageType = typeof ProjectModelsUpdatedMessageType[keyof typeof ProjectModelsUpdatedMessageType];
export type ProjectMoveToWorkspaceDryRun = {
__typename?: 'ProjectMoveToWorkspaceDryRun';
addedToWorkspace: Array<LimitedUser>;
addedToWorkspaceTotalCount: Scalars['Int']['output'];
};
export type ProjectMoveToWorkspaceDryRunAddedToWorkspaceArgs = {
limit?: InputMaybe<Scalars['Int']['input']>;
};
export type ProjectMutations = {
__typename?: 'ProjectMutations';
/** Access request related mutations */
@@ -4058,8 +4076,11 @@ export type UserProjectCollection = {
export type UserProjectsFilter = {
/** Only include projects where user has the specified roles */
onlyWithRoles?: InputMaybe<Array<Scalars['String']['input']>>;
/** Only include personal projects (not in any workspace) */
personalOnly?: InputMaybe<Scalars['Boolean']['input']>;
/** Filter out projects by name */
search?: InputMaybe<Scalars['String']['input']>;
/** Only include projects in the specified workspace */
workspaceId?: InputMaybe<Scalars['ID']['input']>;
};
@@ -4478,6 +4499,8 @@ export type WorkspaceCollection = {
export type WorkspaceCreateInput = {
description?: InputMaybe<Scalars['String']['input']>;
/** Add this domain to the workspace as a verified domain and enable domain discoverability */
enableDomainDiscoverabilityForDomain?: InputMaybe<Scalars['String']['input']>;
/** Logo image as base64-encoded string */
logo?: InputMaybe<Scalars['String']['input']>;
name: Scalars['String']['input'];
@@ -4841,6 +4864,8 @@ export type WorkspaceProjectMutationsUpdateRoleArgs = {
export type WorkspaceProjectsFilter = {
/** Filter out projects by name */
search?: InputMaybe<Scalars['String']['input']>;
/** Only return workspace projects that the active user has an explicit project role in */
withProjectRoleOnly?: InputMaybe<Scalars['Boolean']['input']>;
};
export type WorkspaceProjectsUpdatedMessage = {
@@ -5226,6 +5251,7 @@ export type ResolversTypes = {
ProjectModelsTreeFilter: ProjectModelsTreeFilter;
ProjectModelsUpdatedMessage: ResolverTypeWrapper<Omit<ProjectModelsUpdatedMessage, 'model'> & { model?: Maybe<ResolversTypes['Model']> }>;
ProjectModelsUpdatedMessageType: ProjectModelsUpdatedMessageType;
ProjectMoveToWorkspaceDryRun: ResolverTypeWrapper<ProjectMoveToWorkspaceDryRunGraphQLReturn>;
ProjectMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
ProjectPendingModelsUpdatedMessage: ResolverTypeWrapper<Omit<ProjectPendingModelsUpdatedMessage, 'model'> & { model: ResolversTypes['FileUpload'] }>;
ProjectPendingModelsUpdatedMessageType: ProjectPendingModelsUpdatedMessageType;
@@ -5545,6 +5571,7 @@ export type ResolversParentTypes = {
ProjectModelsFilter: ProjectModelsFilter;
ProjectModelsTreeFilter: ProjectModelsTreeFilter;
ProjectModelsUpdatedMessage: Omit<ProjectModelsUpdatedMessage, 'model'> & { model?: Maybe<ResolversParentTypes['Model']> };
ProjectMoveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRunGraphQLReturn;
ProjectMutations: MutationsObjectGraphQLReturn;
ProjectPendingModelsUpdatedMessage: Omit<ProjectPendingModelsUpdatedMessage, 'model'> & { model: ResolversParentTypes['FileUpload'] };
ProjectPendingVersionsUpdatedMessage: Omit<ProjectPendingVersionsUpdatedMessage, 'version'> & { version: ResolversParentTypes['FileUpload'] };
@@ -6458,6 +6485,7 @@ export type ProjectResolvers<ContextType = GraphQLContext, ParentType extends Re
modelChildrenTree?: Resolver<Array<ResolversTypes['ModelsTreeItem']>, ParentType, ContextType, RequireFields<ProjectModelChildrenTreeArgs, 'fullName'>>;
models?: Resolver<ResolversTypes['ModelCollection'], ParentType, ContextType, RequireFields<ProjectModelsArgs, 'limit'>>;
modelsTree?: Resolver<ResolversTypes['ModelsTreeItemCollection'], ParentType, ContextType, RequireFields<ProjectModelsTreeArgs, 'limit'>>;
moveToWorkspaceDryRun?: Resolver<ResolversTypes['ProjectMoveToWorkspaceDryRun'], ParentType, ContextType, RequireFields<ProjectMoveToWorkspaceDryRunArgs, 'workspaceId'>>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
object?: Resolver<Maybe<ResolversTypes['Object']>, ParentType, ContextType, RequireFields<ProjectObjectArgs, 'id'>>;
pendingAccessRequests?: Resolver<Maybe<Array<ResolversTypes['ProjectAccessRequest']>>, ParentType, ContextType>;
@@ -6564,6 +6592,12 @@ export type ProjectModelsUpdatedMessageResolvers<ContextType = GraphQLContext, P
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ProjectMoveToWorkspaceDryRunResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ProjectMoveToWorkspaceDryRun'] = ResolversParentTypes['ProjectMoveToWorkspaceDryRun']> = {
addedToWorkspace?: Resolver<Array<ResolversTypes['LimitedUser']>, ParentType, ContextType, Partial<ProjectMoveToWorkspaceDryRunAddedToWorkspaceArgs>>;
addedToWorkspaceTotalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ProjectMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ProjectMutations'] = ResolversParentTypes['ProjectMutations']> = {
accessRequestMutations?: Resolver<ResolversTypes['ProjectAccessRequestMutations'], ParentType, ContextType>;
automationMutations?: Resolver<ResolversTypes['ProjectAutomationMutations'], ParentType, ContextType, RequireFields<ProjectMutationsAutomationMutationsArgs, 'projectId'>>;
@@ -7492,6 +7526,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
ProjectFileImportUpdatedMessage?: ProjectFileImportUpdatedMessageResolvers<ContextType>;
ProjectInviteMutations?: ProjectInviteMutationsResolvers<ContextType>;
ProjectModelsUpdatedMessage?: ProjectModelsUpdatedMessageResolvers<ContextType>;
ProjectMoveToWorkspaceDryRun?: ProjectMoveToWorkspaceDryRunResolvers<ContextType>;
ProjectMutations?: ProjectMutationsResolvers<ContextType>;
ProjectPendingModelsUpdatedMessage?: ProjectPendingModelsUpdatedMessageResolvers<ContextType>;
ProjectPendingVersionsUpdatedMessage?: ProjectPendingVersionsUpdatedMessageResolvers<ContextType>;
@@ -6,7 +6,6 @@ import {
insertCommentsFactory
} from '@/modules/comments/repositories/comments'
import { RateLimitError } from '@/modules/core/errors/ratelimit'
import { StreamNotFoundError } from '@/modules/core/errors/stream'
import {
ProjectVisibility,
Resolvers,
@@ -88,10 +87,7 @@ import {
UserSubscriptions
} from '@/modules/shared/utils/subscriptions'
import { has } from 'lodash'
import { throwUncoveredError } from '@speckle/shared'
import { ForbiddenError } from '@/modules/shared/errors'
import { Authz } from '@speckle/shared'
import { SsoSessionMissingOrExpiredError } from '@/modules/workspacesCore/errors'
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
const getServerInfo = getServerInfoFactory({ db })
const getUsers = getUsersFactory({ db })
@@ -184,27 +180,7 @@ export = {
projectId: args.id,
userId: context.userId
})
if (!canQuery.isOk) {
switch (canQuery.error.code) {
case Authz.ProjectNotFoundError.code:
throw new StreamNotFoundError('Project not found')
case Authz.ProjectNoAccessError.code:
case Authz.WorkspaceNoAccessError.code:
throw new ForbiddenError(canQuery.error.message)
case Authz.WorkspaceSsoSessionNoAccessError.code:
throw new SsoSessionMissingOrExpiredError(canQuery.error.message, {
info: {
workspaceSlug: canQuery.error.payload.workspaceSlug
}
})
case Authz.ServerNoAccessError.code:
case Authz.ServerNoSessionError.code:
throw new ForbiddenError(canQuery.error.message)
default:
throwUncoveredError(canQuery.error)
}
}
throwIfAuthNotOk(canQuery)
const project = await getStream({ streamId: args.id })
@@ -284,6 +260,11 @@ export = {
throw new RateLimitError(rateLimitResult)
}
const canCreate = await context.authPolicies.project.canCreateLegacy({
userId: context.userId
})
throwIfAuthNotOk(canCreate)
const regionKey = await getValidDefaultProjectRegionKey()
const projectDb = await getDb({ regionKey })
@@ -344,7 +325,8 @@ export = {
searchQuery: args.filter?.search || undefined,
withRoles: (args.filter?.onlyWithRoles || []) as StreamRoles[],
streamIdWhitelist: toProjectIdWhitelist(ctx.resourceAccessRules),
workspaceId: args.filter?.workspaceId
workspaceId: args.filter?.workspaceId,
personalOnly: args.filter?.personalOnly
}),
getUserStreamsCount({
userId: ctx.userId!,
@@ -353,7 +335,8 @@ export = {
withRoles: (args.filter?.onlyWithRoles || []) as StreamRoles[],
streamIdWhitelist: toProjectIdWhitelist(ctx.resourceAccessRules),
onlyWithActiveSsoSession: true,
workspaceId: args.filter?.workspaceId
workspaceId: args.filter?.workspaceId,
personalOnly: args.filter?.personalOnly
}),
getUserStreams({
userId: ctx.userId!,
@@ -364,7 +347,8 @@ export = {
withRoles: (args.filter?.onlyWithRoles || []) as StreamRoles[],
streamIdWhitelist: toProjectIdWhitelist(ctx.resourceAccessRules),
onlyWithActiveSsoSession: true,
workspaceId: args.filter?.workspaceId
workspaceId: args.filter?.workspaceId,
personalOnly: args.filter?.personalOnly
})
])
+24 -18
View File
@@ -1,6 +1,7 @@
import { ApolloServerOptions, BaseContext } from '@apollo/server'
import { Authz } from '@speckle/shared'
import { GraphQLError } from 'graphql'
import _ from 'lodash'
import _, { isObjectLike } from 'lodash'
import { VError } from 'verror'
import { ZodError } from 'zod'
import { fromZodError } from 'zod-validation-error'
@@ -35,29 +36,34 @@ export function buildErrorFormatter(params: {
}
}
// If error isn't a VError child, don't do anything extra
if (!(realError instanceof VError)) {
return formattedError
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let extensions: { [key: string]: any } = {
...(formattedError.extensions || {})
}
// Converting VError based error to Apollo's format
const extensions = {
...(formattedError.extensions || {}),
...(VError.info(realError) || {})
if (realError instanceof VError) {
extensions = _.omit(
{
...extensions,
...(VError.info(realError) || {}),
stacktrace: VError.fullStack(realError)
},
VERROR_TRASH_PROPS
)
} else if (Authz.isAuthPolicyError(realError)) {
extensions = {
...extensions,
code: realError.code,
...(isObjectLike(realError.payload)
? realError.payload
: { payload: realError.payload })
}
}
// Getting rid of redundant info
delete extensions.originalError
// Updating exception metadata in extensions
if (extensions.exception) {
extensions.exception = _.omit(extensions.exception, VERROR_TRASH_PROPS)
if (includeStacktraceInErrorResponses) {
extensions.exception.stacktrace = VError.fullStack(realError)
} else {
delete extensions.exception.stacktrace
}
if (!includeStacktraceInErrorResponses) {
delete extensions.stacktrace
}
return {
@@ -57,7 +57,6 @@ import { metaHelpers } from '@/modules/core/helpers/meta'
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
import {
DeleteProjectRole,
GetProjectCollaborators,
UpdateProject,
GetRolesByUserId,
UpsertProjectRole,
@@ -686,12 +685,6 @@ export const getStreamCollaboratorsFactory =
return items
}
export const getProjectCollaboratorsFactory =
(deps: { db: Knex }): GetProjectCollaborators =>
async ({ projectId }) => {
return await getStreamCollaboratorsFactory(deps)(projectId)
}
/**
* Get base query for finding or counting user streams
*/
@@ -705,7 +698,8 @@ const getUserStreamsQueryBaseFactory =
withRoles,
streamIdWhitelist,
workspaceId,
onlyWithActiveSsoSession
onlyWithActiveSsoSession,
personalOnly
}: BaseUserStreamsQueryParams) => {
const query = tables
.streamAcl(deps.db)
@@ -740,8 +734,10 @@ const getUserStreamsQueryBaseFactory =
})
}
if (!isUndefined(workspaceId)) {
if (workspaceId?.length) {
query.andWhere(Streams.col.workspaceId, workspaceId)
} else if (personalOnly) {
query.andWhere(Streams.col.workspaceId, null)
}
if (ownedOnly || withRoles?.length) {
@@ -157,7 +157,10 @@ export const createUserFactory =
}): CreateValidatedUser =>
async (user, options = undefined) => {
// ONLY ALLOW SKIPPING WHEN CREATING USERS FOR TESTS, IT'S UNSAFE OTHERWISE
const { skipPropertyValidation = false } = options || {}
const {
skipPropertyValidation = false,
allowPersonalEmail = !FF_NO_PERSONAL_EMAILS_ENABLED
} = options || {}
const signUpCtx = user.signUpContext
@@ -175,8 +178,7 @@ export const createUserFactory =
const isBlockedDomain = blockedDomains.includes(
finalUser.email.split('@')[1]?.toLowerCase()
)
const requireWorkDomain =
!user?.signUpContext?.isInvite && FF_NO_PERSONAL_EMAILS_ENABLED
const requireWorkDomain = !user?.signUpContext?.isInvite && !allowPersonalEmail
if (requireWorkDomain && isBlockedDomain) throw new BlockedEmailDomainError()
@@ -1,12 +1,4 @@
/* istanbul ignore file */
const { mockRequireModule } = require('@/test/mockHelper')
const envHelperMock = mockRequireModule(
[
'@/modules/shared/helpers/envHelper',
require.resolve('../../shared/helpers/envHelper')
],
['@/modules/shared/index']
)
const expect = require('chai').expect
const { beforeEachContext } = require('@/test/hooks')
@@ -73,6 +65,7 @@ const {
finalizeInvitedServerRegistrationFactory
} = require('@/modules/serverinvites/services/processing')
const { getServerInfoFactory } = require('@/modules/core/repositories/server')
const { mockAdminOverride } = require('@/test/mocks/global')
const getServerInfo = getServerInfoFactory({ db })
const getUser = getUserFactory({ db })
@@ -133,6 +126,7 @@ const createUser = createUserFactory({
}),
emitEvent: getEventBus().emit
})
const adminOverrideMock = mockAdminOverride()
describe('Generic AuthN & AuthZ controller tests', () => {
before(async () => {
@@ -265,11 +259,10 @@ describe('Generic AuthN & AuthZ controller tests', () => {
})
afterEach(() => {
envHelperMock.disable()
adminOverrideMock.disable()
})
after(() => {
envHelperMock.destroy()
envHelperMock.resetMockedFunctions()
adminOverrideMock.disable()
})
it('should allow stream:owners to be stream:owners', async () => {
await authorizeResolver(
@@ -281,8 +274,7 @@ describe('Generic AuthN & AuthZ controller tests', () => {
})
it('should get the passed in role for server:admins if override enabled', async () => {
envHelperMock.enable()
envHelperMock.mockFunction('adminOverrideEnabled', () => true)
adminOverrideMock.enable(true)
await authorizeResolver(
serverOwner.id,
myStream.id,
@@ -305,8 +297,7 @@ describe('Generic AuthN & AuthZ controller tests', () => {
})
it('should allow server:admins to be anything if adminOverride is enabled', async () => {
envHelperMock.enable()
envHelperMock.mockFunction('adminOverrideEnabled', () => true)
adminOverrideMock.enable(true)
await authorizeResolver(
serverOwner.id,
@@ -331,8 +322,8 @@ describe('Generic AuthN & AuthZ controller tests', () => {
})
it('should not allow server:users to be anything if adminOverride is enabled', async () => {
envHelperMock.enable()
envHelperMock.mockFunction('adminOverrideEnabled', () => true)
adminOverrideMock.enable(true)
try {
await authorizeResolver(
otherGuy.id,
@@ -84,7 +84,7 @@ describe('Projects GraphQL @core', () => {
createProjectNonInWorkspaceRes.data!.projectMutations.create
const userProjectsRes = await apollo.execute(ActiveUserProjectsDocument, {
filter: { workspaceId: null }
filter: { personalOnly: true }
})
expect(userProjectsRes).to.not.haveGraphQLErrors()
@@ -2011,6 +2011,8 @@ export type Project = {
* real or fake (e.g., with a foo/bar model, it will be nested under foo even if such a model doesn't actually exist)
*/
modelsTree: ModelsTreeItemCollection;
/** Returns information about the potential effects of moving a project to a given workspace. */
moveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRun;
name: Scalars['String']['output'];
object?: Maybe<Object>;
/** Pending project access requests */
@@ -2109,6 +2111,11 @@ export type ProjectModelsTreeArgs = {
};
export type ProjectMoveToWorkspaceDryRunArgs = {
workspaceId: Scalars['String']['input'];
};
export type ProjectObjectArgs = {
id: Scalars['String']['input'];
};
@@ -2429,6 +2436,17 @@ export const ProjectModelsUpdatedMessageType = {
} as const;
export type ProjectModelsUpdatedMessageType = typeof ProjectModelsUpdatedMessageType[keyof typeof ProjectModelsUpdatedMessageType];
export type ProjectMoveToWorkspaceDryRun = {
__typename?: 'ProjectMoveToWorkspaceDryRun';
addedToWorkspace: Array<LimitedUser>;
addedToWorkspaceTotalCount: Scalars['Int']['output'];
};
export type ProjectMoveToWorkspaceDryRunAddedToWorkspaceArgs = {
limit?: InputMaybe<Scalars['Int']['input']>;
};
export type ProjectMutations = {
__typename?: 'ProjectMutations';
/** Access request related mutations */
@@ -4038,8 +4056,11 @@ export type UserProjectCollection = {
export type UserProjectsFilter = {
/** Only include projects where user has the specified roles */
onlyWithRoles?: InputMaybe<Array<Scalars['String']['input']>>;
/** Only include personal projects (not in any workspace) */
personalOnly?: InputMaybe<Scalars['Boolean']['input']>;
/** Filter out projects by name */
search?: InputMaybe<Scalars['String']['input']>;
/** Only include projects in the specified workspace */
workspaceId?: InputMaybe<Scalars['ID']['input']>;
};
@@ -4458,6 +4479,8 @@ export type WorkspaceCollection = {
export type WorkspaceCreateInput = {
description?: InputMaybe<Scalars['String']['input']>;
/** Add this domain to the workspace as a verified domain and enable domain discoverability */
enableDomainDiscoverabilityForDomain?: InputMaybe<Scalars['String']['input']>;
/** Logo image as base64-encoded string */
logo?: InputMaybe<Scalars['String']['input']>;
name: Scalars['String']['input'];
@@ -4821,6 +4844,8 @@ export type WorkspaceProjectMutationsUpdateRoleArgs = {
export type WorkspaceProjectsFilter = {
/** Filter out projects by name */
search?: InputMaybe<Scalars['String']['input']>;
/** Only return workspace projects that the active user has an explicit project role in */
withProjectRoleOnly?: InputMaybe<Scalars['Boolean']['input']>;
};
export type WorkspaceProjectsUpdatedMessage = {
@@ -2,7 +2,11 @@ import {
WorkspacePlanProductPrices,
WorkspacePricingProducts
} from '@/modules/gatekeeperCore/domain/billing'
import { Workspace, WorkspaceAcl } from '@/modules/workspacesCore/domain/types'
import {
Workspace,
WorkspaceSeat,
WorkspaceSeatType
} from '@/modules/workspacesCore/domain/types'
import {
Nullable,
Optional,
@@ -16,6 +20,12 @@ import {
import { OverrideProperties } from 'type-fest'
import { z } from 'zod'
export { WorkspaceSeat, WorkspaceSeatType }
export {
GetWorkspaceRoleAndSeat,
GetWorkspaceRolesAndSeats
} from '@/modules/workspacesCore/domain/operations'
export type GetWorkspacePlan = (args: {
workspaceId: string
}) => Promise<WorkspacePlan | null>
@@ -199,20 +209,6 @@ export type ReconcileSubscriptionData = (args: {
prorationBehavior: 'always_invoice' | 'create_prorations' | 'none'
}) => Promise<void>
export const WorkspaceSeatType = <const>{
Viewer: 'viewer',
Editor: 'editor'
}
export type WorkspaceSeatType =
(typeof WorkspaceSeatType)[keyof typeof WorkspaceSeatType]
export type WorkspaceSeat = {
workspaceId: string
userId: string
type: WorkspaceSeatType
createdAt: Date
updatedAt: Date
}
// Prices
export type GetRecurringPrices = () => Promise<
{
@@ -224,26 +220,3 @@ export type GetRecurringPrices = () => Promise<
>
export type GetWorkspacePlanProductPrices = () => Promise<WorkspacePlanProductPrices>
export type GetWorkspaceRolesAndSeats = (params: {
workspaceId: string
userIds?: string[]
}) => Promise<{
[userId: string]: {
role: WorkspaceAcl
seat: Nullable<WorkspaceSeat>
userId: string
}
}>
export type GetWorkspaceRoleAndSeat = (params: {
workspaceId: string
userId: string
}) => Promise<
| {
role: WorkspaceAcl
seat: Nullable<WorkspaceSeat>
userId: string
}
| undefined
>
@@ -1,10 +1,6 @@
import { buildTableHelper, StreamAcl, Streams } from '@/modules/core/dbSchema'
import { StreamAcl, Streams } from '@/modules/core/dbSchema'
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
import {
GetWorkspaceRoleAndSeat,
GetWorkspaceRolesAndSeats,
WorkspaceSeat
} from '@/modules/gatekeeper/domain/billing'
import { WorkspaceSeat } from '@/modules/gatekeeper/domain/billing'
import {
CountSeatsByTypeInWorkspace,
CreateWorkspaceSeat,
@@ -14,18 +10,13 @@ import {
GetWorkspaceUserSeat,
GetWorkspaceUserSeats
} from '@/modules/gatekeeper/domain/operations'
import { formatJsonArrayRecords } from '@/modules/shared/helpers/dbHelper'
import { WorkspaceAcl as WorkspaceAclRecord } from '@/modules/workspacesCore/domain/types'
import { WorkspaceAcl } from '@/modules/workspacesCore/helpers/db'
import { WorkspaceAcl, WorkspaceSeats } from '@/modules/workspacesCore/helpers/db'
import { Knex } from 'knex'
const WorkspaceSeats = buildTableHelper('workspace_seats', [
'workspaceId',
'userId',
'type',
'createdAt',
'updatedAt'
])
export {
getWorkspaceRoleAndSeatFactory,
getWorkspaceRolesAndSeatsFactory
} from '@/modules/workspacesCore/repositories/rolesSeats'
const tables = {
workspaceSeats: (db: Knex) => db<WorkspaceSeat>(WorkspaceSeats.name),
@@ -93,55 +84,6 @@ export const getWorkspaceUserSeatFactory =
return seats[userId]
}
export const getWorkspaceRolesAndSeatsFactory =
(deps: { db: Knex }): GetWorkspaceRolesAndSeats =>
async ({ workspaceId, userIds }) => {
const q = tables
.workspaceAcl(deps.db)
.select<Array<{ seats: WorkspaceSeat[]; roles: WorkspaceAclRecord[] }>>([
// There's only ever gonna be 1 role and seat per user, but this way we can avoid having to group
// by many columns and we can get everything in 1 query
WorkspaceAcl.groupArray('roles'),
WorkspaceSeats.groupArray('seats')
])
.leftJoin(WorkspaceSeats.name, (j1) => {
j1.on(WorkspaceSeats.col.userId, WorkspaceAcl.col.userId).andOnVal(
WorkspaceSeats.col.workspaceId,
workspaceId
)
})
.where(WorkspaceAcl.col.workspaceId, workspaceId)
.groupBy(WorkspaceAcl.col.userId)
if (userIds?.length) {
q.whereIn(WorkspaceAcl.col.userId, userIds)
}
const res = await q
return res.reduce((acc, row) => {
const role = formatJsonArrayRecords(row.roles)[0]
if (!role) return acc
acc[role.userId] = {
role,
seat: formatJsonArrayRecords(row.seats || [])[0] || null,
userId: role.userId
}
return acc
}, {} as Awaited<ReturnType<GetWorkspaceRolesAndSeats>>)
}
export const getWorkspaceRoleAndSeatFactory =
(deps: { db: Knex }): GetWorkspaceRoleAndSeat =>
async ({ workspaceId, userId }) => {
const getWorkspaceRolesAndSeats = getWorkspaceRolesAndSeatsFactory(deps)
const rolesAndSeats = await getWorkspaceRolesAndSeats({
workspaceId,
userIds: [userId]
})
return rolesAndSeats[userId]
}
export const getWorkspacesUsersSeatsFactory =
(deps: { db: Knex }): GetWorkspacesUsersSeats =>
async (params) => {
+2
View File
@@ -138,6 +138,8 @@ export const init = async (params: { app: Express; metricsRegister: Registry })
await module.finalize?.({ app, isInitial, metricsRegister })
}
// Reset some caches
// Validate & cache authz loaders
await moduleAuthLoaders({
dataLoaders: undefined
+2 -2
View File
@@ -9,7 +9,7 @@ export const commandFactory =
}: {
db: Knex
eventBus?: EventBus
operationFactory: (arg: { db: Knex; emit: EventBusEmit }) => TOperation
operationFactory: (arg: { db: Knex; trx: Knex; emit: EventBusEmit }) => TOperation
}) =>
async (...args: Parameters<TOperation>): Promise<Awaited<ReturnType<TOperation>>> => {
const events: EmitArg[] = []
@@ -19,7 +19,7 @@ export const commandFactory =
const trx = await db.transaction()
try {
const result = await operationFactory({ db, emit })(...args)
const result = await operationFactory({ db, trx, emit })(...args)
await trx.commit()
if (eventBus) {
@@ -1,4 +1,8 @@
import { ensureError } from '@speckle/shared'
import { StreamNotFoundError } from '@/modules/core/errors/stream'
import { WorkspacesModuleDisabledError } from '@/modules/core/errors/workspaces'
import { BaseError, ForbiddenError } from '@/modules/shared/errors'
import { SsoSessionMissingOrExpiredError } from '@/modules/workspacesCore/errors'
import { Authz, ensureError, throwUncoveredError } from '@speckle/shared'
import { VError } from 'verror'
/**
@@ -15,3 +19,38 @@ export function getCause(e: Error) {
}
export { ensureError }
/**
* Global mapping for mapping any kind of auth error to a server thrown error
*/
export const mapAuthToServerError = (e: Authz.AllAuthErrors): BaseError => {
switch (e.code) {
case Authz.ProjectNotFoundError.code:
return new StreamNotFoundError(e.message)
case Authz.ProjectNoAccessError.code:
case Authz.WorkspaceNoAccessError.code:
case Authz.WorkspaceNotEnoughPermissionsError.code:
case Authz.WorkspaceReadOnlyError.code:
case Authz.WorkspaceLimitsReachedError.code:
case Authz.WorkspaceNoEditorSeatError.code:
return new ForbiddenError(e.message)
case Authz.WorkspaceSsoSessionNoAccessError.code:
throw new SsoSessionMissingOrExpiredError(e.message, {
info: {
workspaceSlug: e.payload.workspaceSlug
}
})
case Authz.ServerNoAccessError.code:
case Authz.ServerNoSessionError.code:
return new ForbiddenError(e.message)
case Authz.WorkspacesNotEnabledError.code:
return new WorkspacesModuleDisabledError()
default:
throwUncoveredError(e)
}
}
export const throwIfAuthNotOk = (result: Authz.AuthPolicyResult) => {
if (result.isOk) return
throw mapAuthToServerError(result.error)
}
+3 -1
View File
@@ -17,6 +17,7 @@ import {
CommitSubscriptions,
BranchSubscriptions
} from '@/modules/shared/utils/subscriptions'
import { getWorkspaceRoleAndSeatFactory } from '@/modules/workspacesCore/repositories/rolesSeats'
export {
pubsub,
@@ -32,5 +33,6 @@ export const authorizeResolver = authorizeResolverFactory({
getUserServerRole: getUserServerRoleFactory({ db }),
getStream: getStreamFactory({ db }),
getUserAclRole: getUserAclRoleFactory({ db }),
emitWorkspaceEvent: getEventBus().emit
emitWorkspaceEvent: getEventBus().emit,
getWorkspaceRoleAndSeat: getWorkspaceRoleAndSeatFactory({ db })
})
@@ -15,6 +15,7 @@ import { ForbiddenError } from '@/modules/shared/errors'
import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper'
import { EventBusEmit } from '@/modules/shared/services/eventBus'
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
import { GetWorkspaceRoleAndSeat } from '@/modules/workspacesCore/domain/operations'
import { isNullOrUndefined, Roles } from '@speckle/shared'
import { OperationTypeNode } from 'graphql'
@@ -31,6 +32,12 @@ export const validateScopesFactory = (): ValidateScopes => async (scopes, scope)
throw new ForbiddenError(errMsg, { info: { scope } })
}
const workspaceRoleImplicitProjectRoleMap = <const>{
[Roles.Workspace.Admin]: Roles.Stream.Owner,
[Roles.Workspace.Member]: Roles.Stream.Reviewer,
[Roles.Workspace.Guest]: null
}
/**
* Checks the userId against the resource's acl.
*/
@@ -41,6 +48,7 @@ export const authorizeResolverFactory =
getUserServerRole: GetUserServerRole
getStream: GetStream
getUserAclRole: GetUserAclRole
getWorkspaceRoleAndSeat: GetWorkspaceRoleAndSeat
emitWorkspaceEvent: EventBusEmit
}): AuthorizeResolver =>
async (userId, resourceId, requiredRole, userResourceAccessLimits, operationType) => {
@@ -97,7 +105,7 @@ export const authorizeResolverFactory =
targetWorkspaceId = resourceId
}
const userAclRole = userId
let userAclRole = userId
? await deps.getUserAclRole({
aclTableName: role.aclTableName,
userId,
@@ -106,7 +114,27 @@ export const authorizeResolverFactory =
: null
if (!userAclRole) {
throw new ForbiddenError('You are not authorized to access this resource.')
// TODO: Could be more optimized (caching?) but we're moving away from this towards
// auth policies anyway
// Check if workspace role allows for stream actions
if (
role.resourceTarget === RoleResourceTargets.Streams &&
targetWorkspaceId &&
userId
) {
const workspaceRoleAndSeat = await deps.getWorkspaceRoleAndSeat({
workspaceId: targetWorkspaceId,
userId
})
const implicitStreamRole =
workspaceRoleAndSeat?.role.role &&
workspaceRoleImplicitProjectRoleMap[workspaceRoleAndSeat.role.role]
userAclRole = implicitStreamRole
}
if (!userAclRole) {
throw new ForbiddenError('You are not authorized to access this resource.')
}
}
const fullRole = roles.find((r) => r.name === userAclRole)
@@ -117,7 +145,7 @@ export const authorizeResolverFactory =
if (!isNullOrUndefined(targetWorkspaceId)) {
await deps.emitWorkspaceEvent({
eventName: WorkspaceEvents.Authorized,
eventName: WorkspaceEvents.Authorizing,
payload: {
workspaceId: targetWorkspaceId,
userId
@@ -26,6 +26,7 @@ import { TokenResourceIdentifier } from '@/modules/core/domain/tokens/types'
import { ServerRegion } from '@/modules/multiregion/domain/types'
import { SetOptional } from 'type-fest'
import { WorkspaceSeat, WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing'
import { UserRecord } from '@/modules/core/helpers/userHelper'
/** Workspace */
@@ -240,6 +241,51 @@ export type GetWorkspacesProjectsCounts = (params: {
[workspaceId: string]: number
}>
export type GetPaginatedWorkspaceProjectsArgs = {
workspaceId: string
/**
* If set, will take the user's workspace role into account when fetching projects.
* E.g. guests will only see projects they have explicit access to.
*/
userId?: string
cursor?: MaybeNullOrUndefined<string>
/**
* Defaults to 25, if unset
*/
limit?: MaybeNullOrUndefined<number>
filter?: MaybeNullOrUndefined<
Partial<{
/**
* Search for projects by name
*/
search: MaybeNullOrUndefined<string>
/**
* Only get projects that the active user has an explicit role in
*/
withProjectRoleOnly: MaybeNullOrUndefined<boolean>
}>
>
}
export type GetPaginatedWorkspaceProjectsItems = (
params: GetPaginatedWorkspaceProjectsArgs
) => Promise<{
items: Stream[]
cursor: string | null
}>
export type GetPaginatedWorkspaceProjectsTotalCount = (
params: Omit<GetPaginatedWorkspaceProjectsArgs, 'cursor' | 'limit'>
) => Promise<number>
export type GetPaginatedWorkspaceProjects = (
params: GetPaginatedWorkspaceProjectsArgs
) => Promise<{
cursor: string | null
items: Stream[]
totalCount: number
}>
/** Workspace Project Roles */
type GrantWorkspaceProjectRolesArgs = {
@@ -445,3 +491,8 @@ export type SetUserActiveWorkspace = (args: {
/** Is the user in a "personal project" outside of a workspace? */
isProjectsActive?: boolean
}) => Promise<void>
export type IntersectProjectCollaboratorsAndWorkspaceCollaborators = (params: {
projectId: string
workspaceId: string
}) => Promise<UserRecord[]>
@@ -499,7 +499,7 @@ export const workspaceTrackingFactory =
break
case 'gatekeeper.workspace-trial-expired':
break
case 'workspace.authorized':
case WorkspaceEvents.Authorizing:
break
case 'workspace.created':
// we're setting workspace props and attributing to speckle users
@@ -732,7 +732,7 @@ export const initializeEventListenersFactory =
getWorkspaceSubscription: getWorkspaceSubscriptionFactory({ db })
})(payload)
}),
eventBus.listen(WorkspaceEvents.Authorized, async ({ payload }) => {
eventBus.listen(WorkspaceEvents.Authorizing, async ({ payload }) => {
const onWorkspaceAuthorized = onWorkspaceAuthorizedFactory({
getWorkspace,
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db }),
@@ -3,10 +3,12 @@ import { Resolvers } from '@/modules/core/graph/generated/graphql'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { getPaginatedItemsFactory } from '@/modules/shared/services/paginatedItems'
import { WorkspaceTeamMember } from '@/modules/workspaces/domain/types'
import { intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory } from '@/modules/workspaces/repositories/projects'
import {
countInvitableCollaboratorsByProjectIdFactory,
getInvitableCollaboratorsByProjectIdFactory
} from '@/modules/workspaces/repositories/users'
import { getMoveProjectToWorkspaceDryRunFactory } from '@/modules/workspaces/services/projects'
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
@@ -46,6 +48,25 @@ export default FF_WORKSPACES_MODULE_ENABLED
cursor: args.cursor ?? undefined,
limit: args.limit
})
},
moveToWorkspaceDryRun: async (parent, args) => {
const { id: projectId } = parent
const { workspaceId } = args
const { addedToWorkspace } = await getMoveProjectToWorkspaceDryRunFactory({
intersectProjectCollaboratorsAndWorkspaceCollaborators:
intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory({ db })
})({ projectId, workspaceId })
return addedToWorkspace
}
},
ProjectMoveToWorkspaceDryRun: {
addedToWorkspace: async (parent, args) => {
return args.limit ? parent.slice(0, args.limit) : parent
},
addedToWorkspaceTotalCount: async (parent) => {
return parent.length
}
}
} as Resolvers)
@@ -2,7 +2,6 @@ import { db } from '@/db/knex'
import { Resolvers } from '@/modules/core/graph/generated/graphql'
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
import {
getProjectCollaboratorsFactory,
updateProjectFactory,
upsertProjectRoleFactory,
getRolesByUserIdFactory,
@@ -11,8 +10,7 @@ import {
revokeStreamPermissionsFactory,
grantStreamPermissionsFactory,
legacyGetStreamsFactory,
getUserStreamsPageFactory,
getUserStreamsCountFactory
getStreamCollaboratorsFactory
} from '@/modules/core/repositories/streams'
import { InviteCreateValidationError } from '@/modules/serverinvites/errors'
import {
@@ -74,7 +72,8 @@ import {
upsertWorkspaceCreationStateFactory,
queryWorkspacesFactory,
countWorkspacesFactory,
countWorkspaceRoleWithOptionalProjectRoleFactory
countWorkspaceRoleWithOptionalProjectRoleFactory,
getPaginatedWorkspaceProjectsFactory
} from '@/modules/workspaces/repositories/workspaces'
import {
buildWorkspaceInviteEmailContentsFactory,
@@ -98,7 +97,6 @@ import {
} from '@/modules/workspaces/services/management'
import {
createWorkspaceProjectFactory,
getWorkspaceProjectsFactory,
getWorkspaceRoleToDefaultProjectRoleMappingFactory,
moveProjectToWorkspaceFactory,
queryAllWorkspaceProjectsFactory
@@ -288,8 +286,6 @@ const updateStreamRoleAndNotify = updateStreamRoleAndNotifyFactory({
}),
removeStreamCollaborator
})
const getUserStreams = getUserStreamsPageFactory({ db })
const getUserStreamsCount = getUserStreamsCountFactory({ db })
const { FF_WORKSPACES_MODULE_ENABLED, FF_MOVE_PROJECT_REGION_ENABLED } =
getFeatureFlags()
@@ -439,37 +435,90 @@ export = FF_WORKSPACES_MODULE_ENABLED
},
WorkspaceMutations: {
create: async (_parent, args, context) => {
const { name, description, logo, slug } = args.input
const {
name,
description,
logo,
slug,
enableDomainDiscoverabilityForDomain
} = args.input
const createWorkspace = createWorkspaceFactory({
validateSlug: validateSlugFactory({
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db })
}),
generateValidSlug: generateValidSlugFactory({
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db })
}),
upsertWorkspace: upsertWorkspaceFactory({ db }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
emitWorkspaceEvent: getEventBus().emit,
ensureValidWorkspaceRoleSeat: ensureValidWorkspaceRoleSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db }),
getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }),
eventEmit: getEventBus().emit
})
const createWorkspace = commandFactory({
db,
eventBus,
operationFactory: ({ trx, emit }) => {
const createWorkspace = createWorkspaceFactory({
validateSlug: validateSlugFactory({
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: trx })
}),
generateValidSlug: generateValidSlugFactory({
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: trx })
}),
upsertWorkspace: upsertWorkspaceFactory({ db: trx }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db: trx }),
emitWorkspaceEvent: emit,
ensureValidWorkspaceRoleSeat: ensureValidWorkspaceRoleSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db: trx }),
getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db: trx }),
eventEmit: emit
})
})
const updateWorkspace = updateWorkspaceFactory({
validateSlug: validateSlugFactory({
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: trx })
}),
getWorkspace: getWorkspaceWithDomainsFactory({ db: trx }),
getWorkspaceSsoProviderRecord: getWorkspaceSsoProviderFactory({
db: trx,
decrypt: getDecryptor()
}),
upsertWorkspace: upsertWorkspaceFactory({ db: trx }),
emitWorkspaceEvent: emit
})
const addDomain = addDomainToWorkspaceFactory({
getWorkspace: getWorkspaceFactory({ db: trx }),
findEmailsByUserId: findEmailsByUserIdFactory({ db: trx }),
storeWorkspaceDomain: storeWorkspaceDomainFactory({ db: trx }),
getDomains: getWorkspaceDomainsFactory({ db: trx }),
emitWorkspaceEvent: emit
})
return async () => {
let workspace = await createWorkspace({
userId: context.userId!,
workspaceInput: {
name,
slug,
description: description ?? null,
logo: logo ?? null
},
userResourceAccessLimits: context.resourceAccessRules
})
if (enableDomainDiscoverabilityForDomain) {
// Add domain & enable discoverability
await addDomain({
workspaceId: workspace.id,
userId: context.userId!,
domain: enableDomainDiscoverabilityForDomain
})
workspace = await updateWorkspace({
workspaceId: workspace.id,
workspaceInput: {
discoverabilityEnabled: true
}
})
}
return workspace
}
}
})
const workspace = await createWorkspace({
userId: context.userId!,
workspaceInput: {
name,
slug,
description: description ?? null,
logo: logo ?? null
},
userResourceAccessLimits: context.resourceAccessRules
})
return workspace
return await createWorkspace()
},
delete: async (_parent, args, context) => {
const { workspaceId } = args
@@ -1027,7 +1076,7 @@ export = FF_WORKSPACES_MODULE_ENABLED
getProject: getProjectFactory({ db }),
updateProject: updateProjectFactory({ db }),
upsertProjectRole: upsertProjectRoleFactory({ db }),
getProjectCollaborators: getProjectCollaboratorsFactory({ db }),
getProjectCollaborators: getStreamCollaboratorsFactory({ db }),
getWorkspaceRolesAndSeats: getWorkspaceRolesAndSeatsFactory({ db }),
updateWorkspaceRole: updateWorkspaceRoleFactory({
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
@@ -1154,33 +1203,12 @@ export = FF_WORKSPACES_MODULE_ENABLED
return await getPendingTeam({ workspaceId: parent.id, filter: args.filter })
},
projects: async (parent, args, ctx) => {
if (!ctx.userId) return []
const getWorkspaceProjects = getWorkspaceProjectsFactory({
getStreams: getUserStreams
const getWorkspaceProjects = getPaginatedWorkspaceProjectsFactory({ db })
return await getWorkspaceProjects({
workspaceId: parent.id,
userId: ctx.userId!,
...args
})
const filter = {
...(args.filter || {}),
userId: ctx.userId,
workspaceId: parent.id
}
const { items, cursor } = await getWorkspaceProjects(
{
workspaceId: parent.id
},
{
limit: args.limit || 25,
cursor: args.cursor || null,
filter
}
)
return {
items,
cursor,
totalCount: await getUserStreamsCount({
...filter,
searchQuery: filter.search || undefined
})
}
},
automateFunctions: async (parent, args, context) => {
try {
@@ -1519,7 +1547,7 @@ export = FF_WORKSPACES_MODULE_ENABLED
getTotalCount: getWorkspaceCollaboratorsTotalCountFactory({ db })
})({
workspaceId: parent.id,
limit: args.limit,
limit: args.limit ?? 100,
cursor: args.cursor ?? undefined
})
return team
@@ -0,0 +1,27 @@
import { StreamAcl, Users } from '@/modules/core/dbSchema'
import { StreamAclRecord } from '@/modules/core/helpers/types'
import { UserRecord } from '@/modules/core/helpers/userHelper'
import { IntersectProjectCollaboratorsAndWorkspaceCollaborators } from '@/modules/workspaces/domain/operations'
import { WorkspaceAcl } from '@/modules/workspacesCore/helpers/db'
import { Knex } from 'knex'
const tables = {
streamAcl: (db: Knex) => db.table<StreamAclRecord>('stream_acl')
}
export const intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory =
(deps: { db: Knex }): IntersectProjectCollaboratorsAndWorkspaceCollaborators =>
async ({ projectId, workspaceId }) => {
return await tables
.streamAcl(deps.db)
.select<UserRecord[]>(...Users.cols)
.join(Users.name, Users.col.id, StreamAcl.col.userId)
.where(StreamAcl.col.resourceId, projectId)
.except((builder) => {
return builder
.select(...Users.cols)
.from(WorkspaceAcl.name)
.join(Users.name, Users.col.id, WorkspaceAcl.col.userId)
.where(WorkspaceAcl.col.workspaceId, workspaceId)
})
}
@@ -12,6 +12,10 @@ import {
DeleteWorkspace,
DeleteWorkspaceDomain,
DeleteWorkspaceRole,
GetPaginatedWorkspaceProjects,
GetPaginatedWorkspaceProjectsArgs,
GetPaginatedWorkspaceProjectsItems,
GetPaginatedWorkspaceProjectsTotalCount,
GetUserDiscoverableWorkspaces,
GetUserIdsWithRoleInWorkspace,
GetWorkspace,
@@ -36,6 +40,7 @@ import {
import { Knex } from 'knex'
import { Roles } from '@speckle/shared'
import {
ServerAclRecord,
BranchRecord,
StreamAclRecord,
StreamRecord
@@ -60,16 +65,22 @@ import {
InvitesRetrievalValidityFilter
} from '@/modules/serverinvites/repositories/serverInvites'
import { WorkspaceInviteResourceType } from '@/modules/workspacesCore/domain/constants'
import { clamp } from 'lodash'
import { clamp, has, isObjectLike } from 'lodash'
import {
WorkspaceCreationState,
WorkspaceTeamMember
} from '@/modules/workspaces/domain/types'
import {
decodeCompositeCursor,
encodeCompositeCursor
} from '@/modules/shared/helpers/graphqlHelper'
import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper'
const tables = {
branches: (db: Knex) => db<BranchRecord>('branches'),
streams: (db: Knex) => db<StreamRecord>('streams'),
streamAcl: (db: Knex) => db<StreamAclRecord>('stream_acl'),
serverAcl: (db: Knex) => db<ServerAclRecord>(ServerAcl.name),
workspaces: (db: Knex) => db<Workspace>('workspaces'),
workspaceDomains: (db: Knex) => db<WorkspaceDomain>('workspace_domains'),
workspacesAcl: (db: Knex) => db<WorkspaceAcl>('workspace_acl'),
@@ -542,3 +553,152 @@ export const getWorkspacesProjectsCountsFactory =
return ret
}
const getPaginatedWorkspaceProjectsBaseQueryFactory =
(deps: { db: Knex }) =>
(params: Omit<GetPaginatedWorkspaceProjectsArgs, 'cursor' | 'limit'>) => {
const { workspaceId, userId, filter } = params
const { search, withProjectRoleOnly } = filter || {}
const query = tables
.streams(deps.db)
.where(Streams.col.workspaceId, workspaceId)
.select<StreamRecord[]>(Streams.cols)
/**
* If userId is set:
* - If no workspace role, user should be server admin w/ admin override enabled
* - If workspace role is guest, user should have explicit stream roles
* - If workspace role other than guest, just get all workspace streams
*
* If withProjectRoleOnly is set: Require project role always
*/
if (userId) {
query
.leftJoin(DbWorkspaceAcl.name, (j) => {
j.on(DbWorkspaceAcl.col.workspaceId, Streams.col.workspaceId).andOnVal(
DbWorkspaceAcl.col.userId,
userId
)
})
.andWhere((w) => {
// Check server_acl exist first, so subsequent checks can be optimized away
if (adminOverrideEnabled() && !withProjectRoleOnly) {
w.whereExists(
tables
.serverAcl(deps.db)
.select('*')
.where(ServerAcl.col.userId, userId)
.andWhere(ServerAcl.col.role, Roles.Server.Admin)
)
}
w.orWhere((w2) => {
// Ensure workspace role exists and its not guest or the user has explicit stream roles
w2.whereNotNull(DbWorkspaceAcl.col.role).andWhere((w3) => {
if (!withProjectRoleOnly) {
w3.whereNot(DbWorkspaceAcl.col.role, Roles.Workspace.Guest)
}
w3.orWhereExists(
tables
.streamAcl(deps.db)
.select('*')
.where(StreamAcl.col.userId, userId)
.andWhere(StreamAcl.col.resourceId, knex.ref(Streams.col.id))
)
})
})
})
}
if (search?.length) {
query.andWhere((w) => {
w.where(Streams.col.name, 'ILIKE', `%${search}%`).orWhere(
Streams.col.description,
'ILIKE',
`%${search}%`
)
})
}
return query
}
export const getPaginatedWorkspaceProjectsItemsFactory =
(deps: { db: Knex }): GetPaginatedWorkspaceProjectsItems =>
async (params) => {
type CursorType = { updatedAt: string; id: string }
const query = getPaginatedWorkspaceProjectsBaseQueryFactory(deps)(params)
const limit = clamp(params.limit || 25, 1, 50)
const cursor = decodeCompositeCursor<CursorType>(
params.cursor,
(c) => isObjectLike(c) && has(c, 'id') && has(c, 'updatedAt')
)
if (cursor) {
// filter by date, and if there's duplicate dates, filter by id too
query.andWhereRaw('(??, ??) < (?, ?)', [
Streams.col.updatedAt,
Streams.col.id,
cursor.updatedAt,
cursor.id
])
}
query
.orderBy([
{ column: Streams.col.updatedAt, order: 'desc' },
{ column: Streams.col.id, order: 'desc' }
])
.limit(limit)
const rows = await query
const newCursorRow = rows.at(-1)
const newCursor = newCursorRow
? encodeCompositeCursor<CursorType>({
updatedAt: newCursorRow.updatedAt.toISOString(),
id: newCursorRow.id
})
: null
return {
items: rows,
cursor: newCursor
}
}
export const getPaginatedWorkspaceProjectsTotalCountFactory =
(deps: { db: Knex }): GetPaginatedWorkspaceProjectsTotalCount =>
async (params) => {
const query = getPaginatedWorkspaceProjectsBaseQueryFactory(deps)(params)
const [res] = await query.clearSelect().count()
const count = parseInt(res.count.toString())
return count
}
export const getPaginatedWorkspaceProjectsFactory =
(deps: { db: Knex }): GetPaginatedWorkspaceProjects =>
async (params) => {
const getItems = getPaginatedWorkspaceProjectsItemsFactory(deps)
const getTotalCount = getPaginatedWorkspaceProjectsTotalCountFactory(deps)
const [items, totalCount] = await Promise.all([
params.limit !== 0 ? getItems(params) : undefined,
getTotalCount(params)
])
if (!items) {
return {
items: [],
cursor: null,
totalCount
}
}
return {
...items,
totalCount
}
}
@@ -3,6 +3,7 @@ import {
GetDefaultRegion,
GetWorkspaceRoleToDefaultProjectRoleMapping,
GetWorkspaceSeatTypeToProjectRoleMapping,
IntersectProjectCollaboratorsAndWorkspaceCollaborators,
QueryAllWorkspaceProjects,
UpdateWorkspaceRole
} from '@/modules/workspaces/domain/operations'
@@ -13,7 +14,6 @@ import {
} from '@/modules/workspaces/errors/workspace'
import {
GetProject,
GetProjectCollaborators,
UpdateProject,
UpsertProjectRole
} from '@/modules/core/domain/projects/operations'
@@ -22,7 +22,7 @@ import { Roles, StreamRoles } from '@speckle/shared'
import { orderByWeight } from '@/modules/shared/domain/rolesAndScopes/logic'
import coreUserRoles from '@/modules/core/roles'
import {
GetUserStreamsPage,
GetStreamCollaborators,
LegacyGetStreams
} from '@/modules/core/domain/streams/operations'
import { ProjectNotFoundError } from '@/modules/core/errors/projects'
@@ -86,44 +86,6 @@ export const queryAllWorkspaceProjectsFactory = ({
} while (!!cursor)
}
type GetWorkspaceProjectsArgs = {
workspaceId: string
}
type GetWorkspaceProjectsOptions = {
limit: number | null
cursor: string | null
filter: {
search?: string | null
userId: string
}
}
type GetWorkspaceProjectsReturnValue = {
items: StreamRecord[]
cursor: string | null
}
export const getWorkspaceProjectsFactory =
({ getStreams }: { getStreams: GetUserStreamsPage }) =>
async (
args: GetWorkspaceProjectsArgs,
opts: GetWorkspaceProjectsOptions
): Promise<GetWorkspaceProjectsReturnValue> => {
const { streams, cursor } = await getStreams({
cursor: opts.cursor,
limit: opts.limit || 25,
searchQuery: opts.filter?.search || undefined,
workspaceId: args.workspaceId,
userId: opts.filter.userId
})
return {
items: streams,
cursor
}
}
type MoveProjectToWorkspaceArgs = {
projectId: string
workspaceId: string
@@ -144,7 +106,7 @@ export const moveProjectToWorkspaceFactory =
getProject: GetProject
updateProject: UpdateProject
upsertProjectRole: UpsertProjectRole
getProjectCollaborators: GetProjectCollaborators
getProjectCollaborators: GetStreamCollaborators
getWorkspaceRolesAndSeats: GetWorkspaceRolesAndSeats
getWorkspaceRoleToDefaultProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping
updateWorkspaceRole: UpdateWorkspaceRole
@@ -168,7 +130,7 @@ export const moveProjectToWorkspaceFactory =
// Update roles for current project members
const [workspace, projectTeam, workspaceTeam] = await Promise.all([
getWorkspaceWithPlan({ workspaceId }),
getProjectCollaborators({ projectId }),
getProjectCollaborators(projectId),
getWorkspaceRolesAndSeats({ workspaceId })
])
if (!workspace) throw new WorkspaceNotFoundError()
@@ -357,3 +319,17 @@ export const createWorkspaceProjectFactory =
return project
}
export const getMoveProjectToWorkspaceDryRunFactory =
(deps: {
intersectProjectCollaboratorsAndWorkspaceCollaborators: IntersectProjectCollaboratorsAndWorkspaceCollaborators
}) =>
async (args: { projectId: string; workspaceId: string }) => {
const addedToWorkspace =
await deps.intersectProjectCollaboratorsAndWorkspaceCollaborators({
projectId: args.projectId,
workspaceId: args.workspaceId
})
return { addedToWorkspace }
}
@@ -342,12 +342,17 @@ export const unassignFromWorkspaces = async (
}
export const assignToWorkspaces = async (
pairs: [BasicTestWorkspace, BasicTestUser, MaybeNullOrUndefined<WorkspaceRoles>][]
pairs: [
BasicTestWorkspace,
BasicTestUser,
MaybeNullOrUndefined<WorkspaceRoles>,
seatType?: MaybeNullOrUndefined<WorkspaceSeatType>
][]
) => {
// Serial execution is somehow faster with bigger batch sizes, assignToWorkspace
// may be quite heavy on the DB
for (const [workspace, user, role] of pairs) {
await assignToWorkspace(workspace, user, role || undefined)
for (const [workspace, user, role, seatType] of pairs) {
await assignToWorkspace(workspace, user, role || undefined, seatType || undefined)
}
}
@@ -1,25 +1,22 @@
import { db } from '@/db/knex'
import { AllScopes } from '@/modules/core/helpers/mainConstants'
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
import { WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing'
import { getWorkspaceUserSeatsFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
import { WorkspaceInvalidRoleError } from '@/modules/workspaces/errors/workspace'
import {
assignToWorkspace,
assignToWorkspaces,
BasicTestWorkspace,
createTestWorkspace
} from '@/modules/workspaces/tests/helpers/creation'
import { describeEach } from '@/test/assertionHelper'
import {
BasicTestUser,
createAuthTokenForUser,
createTestUser,
createTestUsers
} from '@/test/authHelper'
import { describeEach, itEach } from '@/test/assertionHelper'
import { BasicTestUser, createTestUser, createTestUsers } from '@/test/authHelper'
import {
ActiveUserProjectsWorkspaceDocument,
CreateWorkspaceProjectDocument,
GetProjectDocument,
GetWorkspaceProjectsDocument,
GetWorkspaceProjectsQuery,
GetWorkspaceTeamDocument,
MoveProjectToWorkspaceDocument,
ProjectUpdateRoleInput,
@@ -27,22 +24,26 @@ import {
UpdateWorkspaceProjectRoleDocument
} from '@/test/graphql/generated/graphql'
import {
createTestContext,
ExecuteOperationResponse,
testApolloServer,
TestApolloServer
} from '@/test/graphqlHelper'
import { beforeEachContext } from '@/test/hooks'
import { mockAdminOverride } from '@/test/mocks/global'
import {
addToStream,
BasicTestStream,
createTestStream,
getUserStreamRole
} from '@/test/speckle-helpers/streamHelper'
import { Roles } from '@speckle/shared'
import { isNonNullable, Nullable, Optional, Roles } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
import dayjs from 'dayjs'
import { times } from 'lodash'
const grantStreamPermissions = grantStreamPermissionsFactory({ db })
const adminOverrideMock = mockAdminOverride()
describe('Workspace project GQL CRUD', () => {
let apollo: TestApolloServer
@@ -71,15 +72,8 @@ describe('Workspace project GQL CRUD', () => {
before(async () => {
await beforeEachContext()
await createTestUsers([serverAdminUser, serverMemberUser])
const token = await createAuthTokenForUser(serverAdminUser.id, AllScopes)
apollo = await testApolloServer({
context: await createTestContext({
auth: true,
userId: serverAdminUser.id,
token,
role: serverAdminUser.role,
scopes: AllScopes
})
authUserId: serverAdminUser.id
})
await createTestWorkspace(workspace, serverAdminUser)
@@ -282,72 +276,446 @@ describe('Workspace project GQL CRUD', () => {
})
})
describe('when querying workspace projects', () => {
it('should return multiple projects', async () => {
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: workspace.id
})
describe('when querying projects', () => {
const PAGE_SIZE = 5
const PAGE_COUNT = 3
const TOTAL_COUNT = PAGE_COUNT * PAGE_SIZE
const GUEST_PROJECT_COUNT = PAGE_SIZE + 1
const NON_WORKSPACE_PROJECT_COUNT = 5
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.projects.items.length).to.be.greaterThanOrEqual(3)
const queryWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: '',
name: 'Query Workspace'
}
const workspaceGuest: BasicTestUser = {
id: '',
email: '',
name: 'Query Workspace Guest'
}
const workspaceAdmin = serverMemberUser
const workspaceMember: BasicTestUser = {
id: '',
email: '',
name: 'Query Workspace Member'
}
let projects: BasicTestStream[]
let nonWorkspaceProjects: BasicTestStream[]
let apollo: TestApolloServer
before(async () => {
await createTestUsers([workspaceGuest, workspaceMember])
await createTestWorkspace(queryWorkspace, workspaceAdmin, {
addPlan: { name: 'team', status: 'valid' }
})
await assignToWorkspaces([
[
queryWorkspace,
workspaceGuest,
Roles.Workspace.Guest,
WorkspaceSeatType.Editor
],
[
queryWorkspace,
workspaceMember,
Roles.Workspace.Member,
WorkspaceSeatType.Editor
]
])
projects = times(
TOTAL_COUNT,
(i): BasicTestStream => ({
id: '',
ownerId: '',
name: `Query Workspace Project - #${i}`,
isPublic: false, // have to be private for tests below
workspaceId: queryWorkspace.id
})
)
nonWorkspaceProjects = times(
NON_WORKSPACE_PROJECT_COUNT,
(i): BasicTestStream => ({
id: '',
ownerId: '',
name: `Non Workspace Project - #${i}`,
isPublic: false
})
)
// CREATE CONCURRENTLY TO TEST COMPOSITE CURSOR (same updatedAt)
await Promise.all([
...projects.map((project) => createTestStream(project, workspaceAdmin)),
...nonWorkspaceProjects.map((project) =>
createTestStream(project, workspaceGuest)
)
])
// ONLY ADD EXPLICIT PROJECT ASSIGNMENTS TO GUEST
const projectsToAssign = projects.slice(0, GUEST_PROJECT_COUNT)
await Promise.all(
projectsToAssign.map((project) =>
addToStream(project, workspaceGuest, Roles.Stream.Contributor)
)
)
await Promise.all([
// Add explicit single assignment to workspaceMember to 1st non-workspace project
addToStream(nonWorkspaceProjects[0], workspaceMember, Roles.Stream.Contributor),
// Add explicit single assignment to workspaceMember to 1st workspace project
addToStream(projects[0], workspaceMember, Roles.Stream.Contributor)
])
apollo = await testApolloServer({
authUserId: workspaceAdmin.id
})
})
it('should respect limits', async () => {
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: workspace.id,
limit: 1
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.projects.items.length).to.equal(1)
afterEach(async () => {
adminOverrideMock.disable()
})
it('should respect pagination', async () => {
const resA = await apollo.execute(GetWorkspaceProjectsDocument, {
id: workspace.id,
limit: 10
})
describe('through Workspace.projects', () => {
it('should return all projects for workspace members', async () => {
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: 999 // get everything
})
const resB = await apollo.execute(GetWorkspaceProjectsDocument, {
id: workspace.id,
limit: 10,
cursor: resA.data?.workspace.projects.cursor
})
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.workspace.projects
expect(collection?.items.length).to.equal(TOTAL_COUNT)
expect(collection?.cursor).to.be.ok
expect(collection?.totalCount).to.eq(TOTAL_COUNT)
const projectA = resA.data?.workspace.projects.items[0]
const projectB = resB.data?.workspace.projects.items[0]
expect(resA).to.not.haveGraphQLErrors()
expect(resB).to.not.haveGraphQLErrors()
expect(projectA).to.exist
expect(projectB).to.not.exist
expect(projectA?.name).to.not.equal(projectB?.name)
})
it('should respect search filters', async () => {
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: workspace.id,
limit: 1,
filter: {
search: 'Workspace Project B'
// validate sorting
const projects = collection?.items || []
let lastUpdatedAt: Optional<string> = undefined
for (const project of projects) {
const date = project.updatedAt
if (!lastUpdatedAt) {
lastUpdatedAt = date
continue
}
expect(
dayjs(date).isSame(dayjs(lastUpdatedAt)) ||
dayjs(date).isBefore(dayjs(lastUpdatedAt))
).to.be.true
lastUpdatedAt = date
}
})
const project = res.data?.workspace.projects.items[0]
itEach(
[{ adminOverrideEnabled: true }, { adminOverrideEnabled: false }],
({ adminOverrideEnabled }) =>
adminOverrideEnabled
? 'should return all projects for server admins if override enabled'
: 'should fail retrieving projects for server admins if no override enabled',
async ({ adminOverrideEnabled }) => {
const apollo = await testApolloServer({
authUserId: serverAdminUser.id
})
expect(res).to.not.haveGraphQLErrors()
expect(project).to.exist
expect(project?.name).to.equal('Workspace Project B')
adminOverrideMock.enable(adminOverrideEnabled)
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: 999 // get everything
})
if (adminOverrideEnabled) {
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.workspace.projects
expect(collection?.items.length).to.equal(TOTAL_COUNT)
expect(collection?.cursor).to.be.ok
expect(collection?.totalCount).to.eq(TOTAL_COUNT)
} else {
expect(res).to.haveGraphQLErrors()
const collection = res.data?.workspace.projects
expect(collection).to.not.be.ok
}
}
)
it('should return only explicitly assigned projects for guests', async () => {
const apollo = await testApolloServer({
authUserId: workspaceGuest.id
})
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: 999 // get everything
})
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.workspace.projects
expect(collection?.items.length).to.equal(GUEST_PROJECT_COUNT)
expect(collection?.cursor).to.be.ok
expect(collection?.totalCount).to.equal(GUEST_PROJECT_COUNT)
})
it('should respect limits', async () => {
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: 1
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.projects.items.length).to.equal(1)
expect(res.data?.workspace.projects.cursor).to.be.ok
expect(res.data?.workspace.projects.totalCount).to.equal(TOTAL_COUNT)
})
it('should only return totalCount if limit === 0', async () => {
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: 0
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.projects.items.length).to.equal(0)
expect(res.data?.workspace.projects.cursor).to.be.null
expect(res.data?.workspace.projects.totalCount).to.equal(TOTAL_COUNT)
})
it('should respect pagination', async () => {
let newCursor: Nullable<string> = null
for (let page = 1; page <= PAGE_COUNT + 1; page++) {
const res: ExecuteOperationResponse<GetWorkspaceProjectsQuery> =
await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: PAGE_SIZE,
cursor: newCursor
})
newCursor = res.data?.workspace.projects.cursor || null
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.projects.totalCount).to.equal(TOTAL_COUNT)
if (page <= PAGE_COUNT) {
expect(res.data?.workspace.projects.items.length).to.equal(PAGE_SIZE)
expect(res.data?.workspace.projects.cursor).to.be.ok
} else {
expect(res.data?.workspace.projects.items.length).to.eq(0)
expect(res.data?.workspace.projects.cursor).to.be.null
}
}
})
it('should respect search filters', async () => {
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
filter: {
search: 'Query Workspace Project - #0'
}
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.projects.items.length).to.equal(1)
expect(res.data?.workspace.projects.totalCount).to.equal(1)
expect(res.data?.workspace.projects.cursor).to.be.ok
const project = res.data?.workspace.projects.items[0]
expect(project).to.exist
expect(project?.name).to.equal('Query Workspace Project - #0')
})
it('should respect withProjectRoleOnly flag', async () => {
const apollo = await testApolloServer({
authUserId: workspaceMember.id
})
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
filter: {
withProjectRoleOnly: true
}
})
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.workspace.projects
expect(collection).to.be.ok
expect(collection?.items.length).to.equal(1)
expect(collection?.items[0].id).to.equal(projects[0].id)
expect(collection?.totalCount).to.equal(1)
})
})
it('should return workspace info on project types', async () => {
const res = await apollo.execute(ActiveUserProjectsWorkspaceDocument, {})
describe('for a specific one', () => {
const randomServerGuy: BasicTestUser = {
id: '',
name: 'Random Server Guy',
email: ''
}
const projects = res.data?.activeUser?.projects.items
before(async () => {
await createTestUser(randomServerGuy)
})
expect(res).to.not.haveGraphQLErrors()
expect(projects).to.exist
expect(projects?.every((project) => !!project?.workspace?.id)).to.be.ok
// projects at the end have no explicit project assignments,
// and first X ones are explicitly assigned to guest user
const implicitProject = () => projects.at(-1)!
const explicitGuestProject = () => projects.at(0)!
it('it should be accessible to workspace member', async () => {
const apollo = await testApolloServer({
authUserId: workspaceMember.id
})
const res = await apollo.execute(GetProjectDocument, {
id: implicitProject().id
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.project.id).to.be.ok
})
it('it should not be accessible to random outside workspace guy', async () => {
const apollo = await testApolloServer({
authUserId: randomServerGuy.id
})
const res = await apollo.execute(GetProjectDocument, {
id: implicitProject().id
})
expect(res).to.haveGraphQLErrors()
expect(res.data?.project).to.not.be.ok
})
itEach(
[{ explicit: false }, { explicit: true }],
({ explicit }) =>
explicit
? 'it should be accessible to workspace guest with explicit project role'
: 'it should not be accessible to workspace guest without explicit project role',
async ({ explicit }) => {
const apollo = await testApolloServer({
authUserId: workspaceGuest.id
})
const res = await apollo.execute(GetProjectDocument, {
id: explicit ? explicitGuestProject().id : implicitProject().id
})
if (explicit) {
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.project.id).to.be.ok
} else {
expect(res).to.haveGraphQLErrors()
expect(res.data?.project).to.not.be.ok
}
}
)
itEach(
[{ adminOverrideEnabled: true }, { adminOverrideEnabled: false }],
({ adminOverrideEnabled }) =>
adminOverrideEnabled
? 'it should return project for server admins if override enabled'
: 'it should not return project for server admins if override disabled',
async ({ adminOverrideEnabled }) => {
const apollo = await testApolloServer({
authUserId: serverAdminUser.id
})
adminOverrideMock.enable(adminOverrideEnabled)
const res = await apollo.execute(GetProjectDocument, {
id: implicitProject().id
})
if (adminOverrideEnabled) {
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.project.id).to.be.ok
} else {
expect(res).to.haveGraphQLErrors()
expect(res.data?.project).to.not.be.ok
}
}
)
})
describe('through ActiveUser.projects', () => {
let apollo: TestApolloServer
before(async () => {
apollo = await testApolloServer({
authUserId: workspaceGuest.id
})
})
it('should return all projects user is explicitly assigned to', async () => {
// guest
const apolloGuest = await testApolloServer({
authUserId: workspaceGuest.id
})
const guestRes = await apolloGuest.execute(
ActiveUserProjectsWorkspaceDocument,
{ limit: 999 },
{ assertNoErrors: true }
)
const guestCollection = guestRes.data?.activeUser?.projects
const expectedGuestCount = GUEST_PROJECT_COUNT + NON_WORKSPACE_PROJECT_COUNT
expect(guestCollection).to.be.ok
expect(guestCollection!.totalCount).to.equal(expectedGuestCount)
expect(guestCollection!.items.length).to.equal(expectedGuestCount)
expect(
guestCollection!.items.map((i) => i.workspace?.id).filter(isNonNullable)
).to.have.length(GUEST_PROJECT_COUNT)
// member
const apolloMember = await testApolloServer({
authUserId: workspaceMember.id
})
const memberRes = await apolloMember.execute(
ActiveUserProjectsWorkspaceDocument,
{ limit: 999 },
{ assertNoErrors: true }
)
const memberCollection = memberRes.data?.activeUser?.projects
const expectedMemberCount = 2 // only 2 explicit assignments
expect(memberCollection).to.be.ok
expect(memberCollection!.totalCount).to.equal(expectedMemberCount)
expect(memberCollection!.items.length).to.equal(expectedMemberCount)
expect([
memberCollection!.items[0].id,
memberCollection!.items[1].id
]).to.deep.equalInAnyOrder([nonWorkspaceProjects[0].id, projects[0].id])
})
it('should only return workspace projects if filter set', async () => {
const res = await apollo.execute(ActiveUserProjectsWorkspaceDocument, {
filter: {
workspaceId: queryWorkspace.id
},
limit: 999
})
const expectedCount = GUEST_PROJECT_COUNT
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.activeUser?.projects
expect(collection).to.be.ok
expect(collection?.items.length).to.equal(expectedCount)
expect(collection?.totalCount).to.equal(expectedCount)
expect(
collection?.items.map((i) => i.workspace?.id).filter(isNonNullable)
).to.have.length(expectedCount)
})
it('should only return non-workspace projects if filter set', async () => {
const res = await apollo.execute(ActiveUserProjectsWorkspaceDocument, {
filter: {
personalOnly: true
},
limit: 999
})
const expectedCount = NON_WORKSPACE_PROJECT_COUNT
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.activeUser?.projects
expect(collection).to.be.ok
expect(collection?.items.length).to.equal(expectedCount)
expect(collection?.totalCount).to.equal(expectedCount)
expect(
collection?.items.map((i) => i.workspace?.id).filter((v) => !v)
).to.have.length(expectedCount)
})
})
})
@@ -0,0 +1,126 @@
import { createRandomEmail } from '@/modules/core/helpers/testHelpers'
import { intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory } from '@/modules/workspaces/repositories/projects'
import {
assignToWorkspaces,
BasicTestWorkspace,
createTestWorkspace
} from '@/modules/workspaces/tests/helpers/creation'
import { BasicTestUser, createTestUser, createTestUsers } from '@/test/authHelper'
import {
addAllToStream,
BasicTestStream,
createTestStream
} from '@/test/speckle-helpers/streamHelper'
import cryptoRandomString from 'crypto-random-string'
import { db } from '@/db/knex'
import { expect } from 'chai'
describe('intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory returns a function, that', () => {
const adminUser: BasicTestUser = {
id: '',
email: createRandomEmail(),
name: 'Mr. Workspace'
}
const projectUsers: BasicTestUser[] = [
{
id: '',
email: createRandomEmail(),
name: 'John A. Speckle'
},
{
id: '',
email: createRandomEmail(),
name: 'John B. Speckle'
},
{
id: '',
email: createRandomEmail(),
name: 'John C. Speckle'
}
]
const workspaceUsers: BasicTestUser[] = [
{
id: '',
email: createRandomEmail(),
name: 'John X. Speckle'
},
{
id: '',
email: createRandomEmail(),
name: 'John Y. Speckle'
},
{
id: '',
email: createRandomEmail(),
name: 'John Z. Speckle'
}
]
const project: BasicTestStream = {
id: '',
ownerId: '',
name: cryptoRandomString({ length: 9 }),
isPublic: true
}
const workspace: BasicTestWorkspace = {
id: '',
ownerId: '',
name: cryptoRandomString({ length: 9 }),
slug: ''
}
before(async () => {
await createTestUser(adminUser)
await createTestUsers([...projectUsers, ...workspaceUsers])
await createTestStream(project, adminUser)
await addAllToStream(project, projectUsers)
await createTestWorkspace(workspace, adminUser)
await assignToWorkspaces(workspaceUsers.map((user) => [workspace, user, null]))
})
it('returns users that are project members but not members of the target workspace', async () => {
const result = await intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory({
db
})({
projectId: project.id,
workspaceId: workspace.id
})
expect(result.length).to.equal(3)
expect(
result.every((resultUser) =>
projectUsers.some((projectUser) => projectUser.id === resultUser.id)
)
).to.equal(true)
})
it('does not return project users that are already members of the workspace', async () => {
const result = await intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory({
db
})({
projectId: project.id,
workspaceId: workspace.id
})
expect(
result.some((resultUser) =>
workspaceUsers.some((workspaceUser) => workspaceUser.id === resultUser.id)
)
).to.equal(false)
})
it('does not return workspace admin or project owner', async () => {
const result = await intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory({
db
})({
projectId: project.id,
workspaceId: workspace.id
})
expect(result.some((user) => user.id === adminUser.id)).to.equal(false)
})
})
@@ -56,11 +56,46 @@ import {
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { getUserFactory } from '@/modules/core/repositories/users'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { sendEmail } from '@/modules/emails/services/sending'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { itEach } from '@/test/assertionHelper'
import { assignWorkspaceSeatFactory } from '@/modules/workspaces/services/workspaceSeat'
import { createWorkspaceSeatFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
import { WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing'
const grantStreamPermissions = grantStreamPermissionsFactory({ db })
const validateAndCreateUserEmail = validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser: getUserFactory({ db }),
getServerInfo: getServerInfoFactory({ db }),
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
sendEmail,
renderEmail
})
})
const { FF_GATEKEEPER_FORCE_FREE_PLAN } = getFeatureFlags()
describe('Workspaces GQL CRUD', () => {
@@ -883,6 +918,107 @@ describe('Workspaces GQL CRUD', () => {
expect(getRes.data?.workspace?.name).to.equal(workspaceName)
expect(getRes.data?.workspace?.slug).to.equal(workspaceSlug)
})
describe('when attempting to enable domain discoverability', () => {
const guyWithNoVerifiedEmails: BasicTestUser = {
id: '',
name: 'Guy with no verified emails',
email: 'guy-with-no-verified-emails@bozo1.org',
verified: false
}
const guyWithMultipleVerifiedEmails: BasicTestUser = {
id: '',
name: 'Guy with multiple verified emails',
email: 'guy-with-multiple-verified-emails@bozo2.org',
verified: true
}
const guyWithOneVerifiedEmail: BasicTestUser = {
id: '',
name: 'Guy with one verified email',
email: 'guy-with-one-verified-email@bozo3.org',
verified: true
}
const guyWithOneBlockedVerifiedEmail: BasicTestUser = {
id: '',
name: 'Guy with one blocked verified email',
email: 'guy-with-one-blocked-verified-email@gmail.com',
verified: true,
allowPersonalEmail: true
}
const getDomain = (user: BasicTestUser) => user.email.split('@')[1]
before(async () => {
await createTestUsers([
guyWithNoVerifiedEmails,
guyWithMultipleVerifiedEmails,
guyWithOneVerifiedEmail,
guyWithOneBlockedVerifiedEmail
])
await Promise.all([
validateAndCreateUserEmail({
userEmail: {
userId: guyWithMultipleVerifiedEmails.id,
email: 'guy-with-multiple-verified-emails@bozo22.org',
verified: true
}
}),
validateAndCreateUserEmail({
userEmail: {
userId: guyWithMultipleVerifiedEmails.id,
email: 'guy-with-multiple-verified-emails@bozo23.org',
verified: true
}
})
])
})
itEach(
[guyWithOneVerifiedEmail, guyWithMultipleVerifiedEmails],
(user) => `${user.name} can create with enabled domain discoverability`,
async (user) => {
const apollo = await testApolloServer({
authUserId: user.id
})
const createRes = await apollo.execute(CreateWorkspaceDocument, {
input: {
name: `${user.name} Domain Discoverability Workspace`,
slug: cryptoRandomString({ length: 10 }),
enableDomainDiscoverabilityForDomain: getDomain(user)
}
})
expect(createRes).to.not.haveGraphQLErrors()
expect(createRes.data?.workspaceMutations.create.id).to.be.ok
expect(createRes.data!.workspaceMutations.create.discoverabilityEnabled).to
.be.true
}
)
itEach(
[guyWithNoVerifiedEmails, guyWithOneBlockedVerifiedEmail],
(user) => `${user.name} can not create with enabled domain discoverability`,
async (user) => {
const apollo = await testApolloServer({
authUserId: user.id
})
const createRes = await apollo.execute(CreateWorkspaceDocument, {
input: {
name: `${user.name} Domain Discoverability Workspace`,
slug: cryptoRandomString({ length: 10 }),
enableDomainDiscoverabilityForDomain: getDomain(user)
}
})
expect(createRes).to.haveGraphQLErrors()
expect(createRes.data?.workspaceMutations.create).to.not.be.ok
}
)
})
})
describe('mutation workspaceMutations.delete', () => {
@@ -7,7 +7,7 @@ export const workspaceEventNamespace = 'workspace' as const
const eventPrefix = `${workspaceEventNamespace}.` as const
export const WorkspaceEvents = {
Authorized: `${eventPrefix}authorized`,
Authorizing: `${eventPrefix}authorizing`,
Created: `${eventPrefix}created`,
Updated: `${eventPrefix}updated`,
Deleted: `${eventPrefix}deleted`,
@@ -47,7 +47,7 @@ type WorkspaceJoinedFromDiscoveryPayload = {
}
export type WorkspaceEventsPayloads = {
[WorkspaceEvents.Authorized]: WorkspaceAuthorizedPayload
[WorkspaceEvents.Authorizing]: WorkspaceAuthorizedPayload
[WorkspaceEvents.Created]: WorkspaceCreatedPayload
[WorkspaceEvents.Updated]: WorkspaceUpdatedPayload
[WorkspaceEvents.Deleted]: { workspaceId: string }
@@ -0,0 +1,25 @@
import { WorkspaceAcl, WorkspaceSeat } from '@/modules/workspacesCore/domain/types'
import { Nullable } from '@speckle/shared'
export type GetWorkspaceRolesAndSeats = (params: {
workspaceId: string
userIds?: string[]
}) => Promise<{
[userId: string]: {
role: WorkspaceAcl
seat: Nullable<WorkspaceSeat>
userId: string
}
}>
export type GetWorkspaceRoleAndSeat = (params: {
workspaceId: string
userId: string
}) => Promise<
| {
role: WorkspaceAcl
seat: Nullable<WorkspaceSeat>
userId: string
}
| undefined
>
@@ -72,3 +72,18 @@ export type WorkspaceJoinRequest = {
createdAt: Date
updatedAt: Date
}
export const WorkspaceSeatType = <const>{
Viewer: 'viewer',
Editor: 'editor'
}
export type WorkspaceSeatType =
(typeof WorkspaceSeatType)[keyof typeof WorkspaceSeatType]
export type WorkspaceSeat = {
workspaceId: string
userId: string
type: WorkspaceSeatType
createdAt: Date
updatedAt: Date
}
@@ -36,3 +36,11 @@ export const WorkspaceJoinRequests = buildTableHelper('workspace_join_requests',
'createdAt',
'updatedAt'
])
export const WorkspaceSeats = buildTableHelper('workspace_seats', [
'workspaceId',
'userId',
'type',
'createdAt',
'updatedAt'
])
@@ -40,3 +40,5 @@ export type WorkspaceCollaboratorGraphQLReturn = WorkspaceTeamMember
export type WorkspacePermissionChecksGraphQLReturn = {
workspaceId: string
}
export type ProjectMoveToWorkspaceDryRunGraphQLReturn = LimitedUserRecord[]
@@ -0,0 +1,65 @@
import { formatJsonArrayRecords } from '@/modules/shared/helpers/dbHelper'
import {
GetWorkspaceRoleAndSeat,
GetWorkspaceRolesAndSeats
} from '@/modules/workspacesCore/domain/operations'
import {
WorkspaceSeat,
WorkspaceAcl as WorkspaceAclRecord
} from '@/modules/workspacesCore/domain/types'
import { WorkspaceAcl, WorkspaceSeats } from '@/modules/workspacesCore/helpers/db'
import { Knex } from 'knex'
const tables = {
workspaceSeats: (db: Knex) => db<WorkspaceSeat>(WorkspaceSeats.name),
workspaceAcl: (db: Knex) => db<WorkspaceAclRecord>(WorkspaceAcl.name)
}
export const getWorkspaceRolesAndSeatsFactory =
(deps: { db: Knex }): GetWorkspaceRolesAndSeats =>
async ({ workspaceId, userIds }) => {
const q = tables
.workspaceAcl(deps.db)
.select<Array<{ seats: WorkspaceSeat[]; roles: WorkspaceAclRecord[] }>>([
// There's only ever gonna be 1 role and seat per user, but this way we can avoid having to group
// by many columns and we can get everything in 1 query
WorkspaceAcl.groupArray('roles'),
WorkspaceSeats.groupArray('seats')
])
.leftJoin(WorkspaceSeats.name, (j1) => {
j1.on(WorkspaceSeats.col.userId, WorkspaceAcl.col.userId).andOnVal(
WorkspaceSeats.col.workspaceId,
workspaceId
)
})
.where(WorkspaceAcl.col.workspaceId, workspaceId)
.groupBy(WorkspaceAcl.col.userId)
if (userIds?.length) {
q.whereIn(WorkspaceAcl.col.userId, userIds)
}
const res = await q
return res.reduce((acc, row) => {
const role = formatJsonArrayRecords(row.roles)[0]
if (!role) return acc
acc[role.userId] = {
role,
seat: formatJsonArrayRecords(row.seats || [])[0] || null,
userId: role.userId
}
return acc
}, {} as Awaited<ReturnType<GetWorkspaceRolesAndSeats>>)
}
export const getWorkspaceRoleAndSeatFactory =
(deps: { db: Knex }): GetWorkspaceRoleAndSeat =>
async ({ workspaceId, userId }) => {
const getWorkspaceRolesAndSeats = getWorkspaceRolesAndSeatsFactory(deps)
const rolesAndSeats = await getWorkspaceRolesAndSeats({
workspaceId,
userIds: [userId]
})
return rolesAndSeats[userId]
}
+6 -6
View File
@@ -139,12 +139,12 @@
"@apollo/rover": "^0.23.0",
"@bull-board/express": "^4.2.2",
"@faker-js/faker": "^8.4.1",
"@graphql-codegen/cli": "^5.0.3",
"@graphql-codegen/typed-document-node": "^5.0.11",
"@graphql-codegen/typescript": "^4.1.1",
"@graphql-codegen/typescript-operations": "^4.3.1",
"@graphql-codegen/typescript-resolvers": "^4.4.0",
"@parcel/watcher": "^2.4.1",
"@graphql-codegen/cli": "^5.0.5",
"@graphql-codegen/typed-document-node": "^5.1.1",
"@graphql-codegen/typescript": "^4.1.6",
"@graphql-codegen/typescript-operations": "^4.6.0",
"@graphql-codegen/typescript-resolvers": "^4.5.0",
"@parcel/watcher": "^2.5.1",
"@swc/core": "^1.11.11",
"@tiptap/core": "^2.0.0-beta.176",
"@types/bcrypt": "^5.0.0",
+8 -1
View File
@@ -85,6 +85,10 @@ export type BasicTestUser = {
*/
id: string
role?: ServerRoles
/**
* Even if disabled server-wide, allow personal emails for this user
*/
allowPersonalEmail?: boolean
} & Partial<UserRecord>
const initTestUser = (user: Partial<BasicTestUser>): BasicTestUser => ({
@@ -119,7 +123,10 @@ export async function createTestUser(userObj?: Partial<BasicTestUser>) {
setVal('email', `${kebabCase(baseUser.name)}@example.org`)
}
const id = await createUser(omit(baseUser, ['id']), { skipPropertyValidation: true })
const id = await createUser(omit(baseUser, ['id', 'allowPersonalEmail']), {
skipPropertyValidation: true,
allowPersonalEmail: baseUser.allowPersonalEmail
})
setVal('id', id)
return baseUser
@@ -2012,6 +2012,8 @@ export type Project = {
* real or fake (e.g., with a foo/bar model, it will be nested under foo even if such a model doesn't actually exist)
*/
modelsTree: ModelsTreeItemCollection;
/** Returns information about the potential effects of moving a project to a given workspace. */
moveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRun;
name: Scalars['String']['output'];
object?: Maybe<Object>;
/** Pending project access requests */
@@ -2110,6 +2112,11 @@ export type ProjectModelsTreeArgs = {
};
export type ProjectMoveToWorkspaceDryRunArgs = {
workspaceId: Scalars['String']['input'];
};
export type ProjectObjectArgs = {
id: Scalars['String']['input'];
};
@@ -2430,6 +2437,17 @@ export const ProjectModelsUpdatedMessageType = {
} as const;
export type ProjectModelsUpdatedMessageType = typeof ProjectModelsUpdatedMessageType[keyof typeof ProjectModelsUpdatedMessageType];
export type ProjectMoveToWorkspaceDryRun = {
__typename?: 'ProjectMoveToWorkspaceDryRun';
addedToWorkspace: Array<LimitedUser>;
addedToWorkspaceTotalCount: Scalars['Int']['output'];
};
export type ProjectMoveToWorkspaceDryRunAddedToWorkspaceArgs = {
limit?: InputMaybe<Scalars['Int']['input']>;
};
export type ProjectMutations = {
__typename?: 'ProjectMutations';
/** Access request related mutations */
@@ -4039,8 +4057,11 @@ export type UserProjectCollection = {
export type UserProjectsFilter = {
/** Only include projects where user has the specified roles */
onlyWithRoles?: InputMaybe<Array<Scalars['String']['input']>>;
/** Only include personal projects (not in any workspace) */
personalOnly?: InputMaybe<Scalars['Boolean']['input']>;
/** Filter out projects by name */
search?: InputMaybe<Scalars['String']['input']>;
/** Only include projects in the specified workspace */
workspaceId?: InputMaybe<Scalars['ID']['input']>;
};
@@ -4459,6 +4480,8 @@ export type WorkspaceCollection = {
export type WorkspaceCreateInput = {
description?: InputMaybe<Scalars['String']['input']>;
/** Add this domain to the workspace as a verified domain and enable domain discoverability */
enableDomainDiscoverabilityForDomain?: InputMaybe<Scalars['String']['input']>;
/** Logo image as base64-encoded string */
logo?: InputMaybe<Scalars['String']['input']>;
name: Scalars['String']['input'];
@@ -4822,6 +4845,8 @@ export type WorkspaceProjectMutationsUpdateRoleArgs = {
export type WorkspaceProjectsFilter = {
/** Filter out projects by name */
search?: InputMaybe<Scalars['String']['input']>;
/** Only return workspace projects that the active user has an explicit project role in */
withProjectRoleOnly?: InputMaybe<Scalars['Boolean']['input']>;
};
export type WorkspaceProjectsUpdatedMessage = {
@@ -5948,7 +5973,7 @@ export type MarkProjectVersionReceivedMutationVariables = Exact<{
export type MarkProjectVersionReceivedMutation = { __typename?: 'Mutation', versionMutations: { __typename?: 'VersionMutations', markReceived: boolean } };
export type TestWorkspaceFragment = { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean };
export type TestWorkspaceFragment = { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean };
export type TestWorkspaceCollaboratorFragment = { __typename?: 'WorkspaceCollaborator', id: string, role: string, user: { __typename?: 'LimitedUser', name: string }, projectRoles: Array<{ __typename?: 'ProjectRole', role: string, project: { __typename?: 'Project', id: string, name: string } }> };
@@ -5959,7 +5984,7 @@ export type CreateWorkspaceMutationVariables = Exact<{
}>;
export type CreateWorkspaceMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', create: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean } } };
export type CreateWorkspaceMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', create: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean } } };
export type DeleteWorkspaceMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
@@ -5973,14 +5998,14 @@ export type GetWorkspaceQueryVariables = Exact<{
}>;
export type GetWorkspaceQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, team: { __typename?: 'WorkspaceCollaboratorCollection', items: Array<{ __typename?: 'WorkspaceCollaborator', id: string, role: string, user: { __typename?: 'LimitedUser', name: string }, projectRoles: Array<{ __typename?: 'ProjectRole', role: string, project: { __typename?: 'Project', id: string, name: string } }> }> } } };
export type GetWorkspaceQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean, team: { __typename?: 'WorkspaceCollaboratorCollection', items: Array<{ __typename?: 'WorkspaceCollaborator', id: string, role: string, user: { __typename?: 'LimitedUser', name: string }, projectRoles: Array<{ __typename?: 'ProjectRole', role: string, project: { __typename?: 'Project', id: string, name: string } }> }> } } };
export type GetWorkspaceBySlugQueryVariables = Exact<{
workspaceSlug: Scalars['String']['input'];
}>;
export type GetWorkspaceBySlugQuery = { __typename?: 'Query', workspaceBySlug: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, team: { __typename?: 'WorkspaceCollaboratorCollection', items: Array<{ __typename?: 'WorkspaceCollaborator', id: string, role: string, user: { __typename?: 'LimitedUser', name: string }, projectRoles: Array<{ __typename?: 'ProjectRole', role: string, project: { __typename?: 'Project', id: string, name: string } }> }> } } };
export type GetWorkspaceBySlugQuery = { __typename?: 'Query', workspaceBySlug: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean, team: { __typename?: 'WorkspaceCollaboratorCollection', items: Array<{ __typename?: 'WorkspaceCollaborator', id: string, role: string, user: { __typename?: 'LimitedUser', name: string }, projectRoles: Array<{ __typename?: 'ProjectRole', role: string, project: { __typename?: 'Project', id: string, name: string } }> }> } } };
export type GetActiveUserDiscoverableWorkspacesQueryVariables = Exact<{ [key: string]: never; }>;
@@ -5992,12 +6017,12 @@ export type UpdateWorkspaceMutationVariables = Exact<{
}>;
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', update: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean } } };
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', update: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean } } };
export type GetActiveUserWorkspacesQueryVariables = Exact<{ [key: string]: never; }>;
export type GetActiveUserWorkspacesQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', workspaces: { __typename?: 'WorkspaceCollection', items: Array<{ __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean }> } } | null };
export type GetActiveUserWorkspacesQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', workspaces: { __typename?: 'WorkspaceCollection', items: Array<{ __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean }> } } | null };
export type UpdateWorkspaceRoleMutationVariables = Exact<{
input: WorkspaceRoleUpdateInput;
@@ -6047,10 +6072,14 @@ export type ActiveUserLeaveWorkspaceMutationVariables = Exact<{
export type ActiveUserLeaveWorkspaceMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', leave: boolean } };
export type ActiveUserProjectsWorkspaceQueryVariables = Exact<{ [key: string]: never; }>;
export type ActiveUserProjectsWorkspaceQueryVariables = Exact<{
limit?: InputMaybe<Scalars['Int']['input']>;
cursor?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<UserProjectsFilter>;
}>;
export type ActiveUserProjectsWorkspaceQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', projects: { __typename?: 'UserProjectCollection', items: Array<{ __typename?: 'Project', id: string, workspace?: { __typename?: 'Workspace', id: string, name: string } | null }> } } | null };
export type ActiveUserProjectsWorkspaceQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', projects: { __typename?: 'UserProjectCollection', totalCount: number, items: Array<{ __typename?: 'Project', id: string, workspace?: { __typename?: 'Workspace', id: string, name: string } | null }> } } | null };
export type ActiveUserExpiredSsoSessionsQueryVariables = Exact<{ [key: string]: never; }>;
@@ -6082,7 +6111,7 @@ export const BasicStreamFieldsFragmentDoc = {"kind":"Document","definitions":[{"
export const UserWithEmailsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserWithEmails"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"emails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"primary"}}]}}]}}]} as unknown as DocumentNode<UserWithEmailsFragment, unknown>;
export const BaseUserFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BaseUserFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode<BaseUserFieldsFragment, unknown>;
export const BaseLimitedUserFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BaseLimitedUserFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}}]}}]} as unknown as DocumentNode<BaseLimitedUserFieldsFragment, unknown>;
export const TestWorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<TestWorkspaceFragment, unknown>;
export const TestWorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}}]} as unknown as DocumentNode<TestWorkspaceFragment, unknown>;
export const TestWorkspaceCollaboratorFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<TestWorkspaceCollaboratorFragment, unknown>;
export const TestWorkspaceProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceProject"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]} as unknown as DocumentNode<TestWorkspaceProjectFragment, unknown>;
export const CreateObjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateObject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ObjectCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objectCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"objectInput"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<CreateObjectMutation, CreateObjectMutationVariables>;
@@ -6216,19 +6245,19 @@ export const UserActiveResourcesDocument = {"kind":"Document","definitions":[{"k
export const SetUserActiveWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetUserActiveWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"slug"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"isProjectsActive"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUserMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setActiveWorkspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"slug"}}},{"kind":"Argument","name":{"kind":"Name","value":"isProjectsActive"},"value":{"kind":"Variable","name":{"kind":"Name","value":"isProjectsActive"}}}]}]}}]}}]} as unknown as DocumentNode<SetUserActiveWorkspaceMutation, SetUserActiveWorkspaceMutationVariables>;
export const CreateProjectVersionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateProjectVersion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateVersionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versionMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}}]}}]}}]}}]} as unknown as DocumentNode<CreateProjectVersionMutation, CreateProjectVersionMutationVariables>;
export const MarkProjectVersionReceivedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MarkProjectVersionReceived"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MarkReceivedVersionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versionMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markReceived"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode<MarkProjectVersionReceivedMutation, MarkProjectVersionReceivedMutationVariables>;
export const CreateWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<CreateWorkspaceMutation, CreateWorkspaceMutationVariables>;
export const CreateWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}}]} as unknown as DocumentNode<CreateWorkspaceMutation, CreateWorkspaceMutationVariables>;
export const DeleteWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"delete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}]}]}}]}}]} as unknown as DocumentNode<DeleteWorkspaceMutation, DeleteWorkspaceMutationVariables>;
export const GetWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceCollaborator"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceQuery, GetWorkspaceQueryVariables>;
export const GetWorkspaceBySlugDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceBySlug"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceBySlug"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceCollaborator"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceBySlugQuery, GetWorkspaceBySlugQueryVariables>;
export const GetWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceCollaborator"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceQuery, GetWorkspaceQueryVariables>;
export const GetWorkspaceBySlugDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceBySlug"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceBySlug"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceCollaborator"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceBySlugQuery, GetWorkspaceBySlugQueryVariables>;
export const GetActiveUserDiscoverableWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getActiveUserDiscoverableWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"discoverableWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetActiveUserDiscoverableWorkspacesQuery, GetActiveUserDiscoverableWorkspacesQueryVariables>;
export const UpdateWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"update"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<UpdateWorkspaceMutation, UpdateWorkspaceMutationVariables>;
export const GetActiveUserWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetActiveUserWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<GetActiveUserWorkspacesQuery, GetActiveUserWorkspacesQueryVariables>;
export const UpdateWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"update"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}}]} as unknown as DocumentNode<UpdateWorkspaceMutation, UpdateWorkspaceMutationVariables>;
export const GetActiveUserWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetActiveUserWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}}]} as unknown as DocumentNode<GetActiveUserWorkspacesQuery, GetActiveUserWorkspacesQueryVariables>;
export const UpdateWorkspaceRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspaceRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceRoleUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateRole"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceCollaborator"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<UpdateWorkspaceRoleMutation, UpdateWorkspaceRoleMutationVariables>;
export const CreateWorkspaceProjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateWorkspaceProject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceProjectCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceProject"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceProject"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]} as unknown as DocumentNode<CreateWorkspaceProjectMutation, CreateWorkspaceProjectMutationVariables>;
export const GetWorkspaceProjectsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceProjects"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceProjectsFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceProject"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceProject"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceProjectsQuery, GetWorkspaceProjectsQueryVariables>;
export const GetWorkspaceSsoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceSso"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sso"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"provider"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"session"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"validUntil"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceSsoQuery, GetWorkspaceSsoQueryVariables>;
export const GetWorkspaceTeamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceTeam"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceTeamFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"team"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceCollaborator"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceTeamQuery, GetWorkspaceTeamQueryVariables>;
export const ActiveUserLeaveWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ActiveUserLeaveWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"leave"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]}}]} as unknown as DocumentNode<ActiveUserLeaveWorkspaceMutation, ActiveUserLeaveWorkspaceMutationVariables>;
export const ActiveUserProjectsWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActiveUserProjectsWorkspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<ActiveUserProjectsWorkspaceQuery, ActiveUserProjectsWorkspaceQueryVariables>;
export const ActiveUserProjectsWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActiveUserProjectsWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UserProjectsFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<ActiveUserProjectsWorkspaceQuery, ActiveUserProjectsWorkspaceQueryVariables>;
export const ActiveUserExpiredSsoSessionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActiveUserExpiredSsoSessions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"expiredSsoSessions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode<ActiveUserExpiredSsoSessionsQuery, ActiveUserExpiredSsoSessionsQueryVariables>;
export const MoveProjectToWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MoveProjectToWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"moveToWorkspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}},{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<MoveProjectToWorkspaceMutation, MoveProjectToWorkspaceMutationVariables>;
+8 -2
View File
@@ -10,6 +10,7 @@ export const workspaceFragment = gql`
updatedAt
logo
readOnly
discoverabilityEnabled
}
`
@@ -229,9 +230,14 @@ export const leaveWorkspaceMutation = gql`
`
export const getProjectWorkspaceQuery = gql`
query ActiveUserProjectsWorkspace {
query ActiveUserProjectsWorkspace(
$limit: Int
$cursor: String
$filter: UserProjectsFilter
) {
activeUser {
projects {
projects(filter: $filter, limit: $limit, cursor: $cursor) {
totalCount
items {
id
workspace {
+22
View File
@@ -27,3 +27,25 @@ export const MultiRegionConfigMock = mockRequireModule<
export const StripeClientMock = mockRequireModule<
typeof import('@/modules/gatekeeper/clients/stripe')
>(['@/modules/gatekeeper/clients/stripe'])
export const EnvHelperMock = mockRequireModule<
typeof import('@/modules/shared/helpers/envHelper')
>(
[
'@/modules/shared/helpers/envHelper',
require.resolve('../../modules/shared/helpers/envHelper')
],
['@/modules/shared/index']
)
export const mockAdminOverride = () => {
const enable = (enabled: boolean) => {
EnvHelperMock.mockFunction('adminOverrideEnabled', () => enabled)
}
const disable = () => {
EnvHelperMock.resetMockedFunction('adminOverrideEnabled')
}
return { enable, disable }
}
@@ -109,7 +109,7 @@ export type BasicTestStream = {
export async function createTestStreams(
streamOwnerPairs: [BasicTestStream, BasicTestUser][]
) {
await Promise.all(streamOwnerPairs.map((p) => createTestStream(p[0], p[1])))
return await Promise.all(streamOwnerPairs.map((p) => createTestStream(p[0], p[1])))
}
/**
@@ -145,6 +145,7 @@ export async function createTestStream(
streamObj.id = id
streamObj.ownerId = owner.id
return streamObj
}
export async function leaveStream(streamObj: BasicTestStream, user: BasicTestUser) {
+3
View File
@@ -13,6 +13,9 @@ const configs = [
}
}
},
{
ignores: ['**/html/**']
},
...tseslint.configs.recommendedTypeChecked.map((c) => ({
...c,
files: [...(c.files || []), '**/*.ts', '**/*.d.ts']
@@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest'
import { hasMinimumServerRole, canUseAdminOverride } from './serverRole.js'
import cryptoRandomString from 'crypto-random-string'
import { parseFeatureFlags } from '../../environment/index.js'
describe('hasMinimumServerRole returns a function, that', () => {
it('turns non existing server roles into false ', async () => {
@@ -33,7 +32,7 @@ describe('hasMinimumServerRole returns a function, that', () => {
describe('canUseAdminOverride returns a function, that', () => {
it('returns false for admins if admin override is not enabled', async () => {
const result = await canUseAdminOverride({
getEnv: async () => parseFeatureFlags({}),
getAdminOverrideEnabled: async () => false,
getServerRole: async () => {
expect.fail()
}
@@ -42,21 +41,21 @@ describe('canUseAdminOverride returns a function, that', () => {
})
it('returns false for non admins if admin override is not enabled', async () => {
const result = await canUseAdminOverride({
getEnv: async () => parseFeatureFlags({}),
getAdminOverrideEnabled: async () => false,
getServerRole: async () => 'server:user'
})({ userId: cryptoRandomString({ length: 10 }) })
expect(result).toEqual(false)
})
it('returns false for non admins if admin override is enabled', async () => {
const result = await canUseAdminOverride({
getEnv: async () => parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'true' }),
getAdminOverrideEnabled: async () => true,
getServerRole: async () => 'server:user'
})({ userId: cryptoRandomString({ length: 10 }) })
expect(result).toEqual(false)
})
it('returns true for admins if admin override is enabled', async () => {
const result = await canUseAdminOverride({
getEnv: async () => parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'true' }),
getAdminOverrideEnabled: async () => true,
getServerRole: async () => 'server:admin'
})({ userId: cryptoRandomString({ length: 10 }) })
expect(result).toEqual(true)
@@ -1,5 +1,6 @@
import { Roles, ServerRoles } from '../../core/constants.js'
import { UserContext } from '../domain/context.js'
import { Loaders } from '../domain/loaders.js'
import { isMinimumServerRole } from '../domain/logic/roles.js'
import { AuthPolicyCheck } from '../domain/policies.js'
@@ -15,13 +16,13 @@ export const hasMinimumServerRole: AuthPolicyCheck<
}
export const canUseAdminOverride: AuthPolicyCheck<
'getEnv' | 'getServerRole',
typeof Loaders.getAdminOverrideEnabled | 'getServerRole',
UserContext
> =
(loaders) =>
async ({ userId }) => {
const { FF_ADMIN_OVERRIDE_ENABLED } = await loaders.getEnv()
if (!FF_ADMIN_OVERRIDE_ENABLED) return false
const adminOverrideEnabled = await loaders.getAdminOverrideEnabled()
if (!adminOverrideEnabled) return false
return await hasMinimumServerRole(loaders)({
userId,
role: Roles.Server.Admin
+22 -2
View File
@@ -1,10 +1,12 @@
import { get, isObjectLike } from '#lodash'
import { ValueOf } from 'type-fest'
import { WorkspaceLimits } from '../../workspaces/helpers/limits.js'
export type AuthError<ErrorCode extends string = string, Payload = undefined> = {
readonly code: ErrorCode
readonly message: string
readonly payload: Payload
}
} & Error
export const defineAuthError = <
ErrorCode extends string,
@@ -20,10 +22,11 @@ export const defineAuthError = <
): AuthError<ErrorCode, Payload>
code: ErrorCode
} => {
return class AuthErrorClass {
return class AuthErrorClass extends Error {
readonly message: string
readonly code: ErrorCode
readonly payload: Payload
readonly isAuthPolicyError = true
static code: ErrorCode = definition.code
@@ -33,15 +36,22 @@ export const defineAuthError = <
: [params: { payload: Payload; message?: string }]
) {
const [params] = args
const message = params?.message || definition.message
super(message)
this.code = definition.code
this.payload =
params && 'payload' in params ? params.payload : (undefined as Payload)
this.message = params?.message || definition.message
this.name = definition.code + 'Error'
}
}
}
export const isAuthPolicyError = (err: unknown): err is AuthError => {
return isObjectLike(err) && get(err, 'isAuthPolicyError') === true
}
export const ProjectNotFoundError = defineAuthError({
code: 'ProjectNotFound',
message: 'Project not found'
@@ -104,3 +114,13 @@ export const ServerNoSessionError = defineAuthError({
code: 'ServerNoSession',
message: 'You are not logged in to this server'
})
// Resolve all exported error types
export type AllAuthErrors = ValueOf<{
[key in keyof typeof import('./authErrors.js')]: typeof import('./authErrors.js')[key] extends new (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: any[]
) => infer R
? R
: never
}>
+5 -1
View File
@@ -3,6 +3,7 @@ import { MaybeAsync } from '../../core/index.js'
import type { GetServerRole } from './core/operations.js'
import type { GetProject, GetProjectRole } from './projects/operations.js'
import type {
GetAdminOverrideEnabled,
GetEnv,
GetWorkspace,
GetWorkspaceLimits,
@@ -54,8 +55,10 @@ export const AuthCheckContextLoaderKeys = <const>{
getWorkspacePlan: 'getWorkspacePlan',
getWorkspaceLimits: 'getWorkspaceLimits',
getWorkspaceSsoProvider: 'getWorkspaceSsoProvider',
getWorkspaceSsoSession: 'getWorkspaceSsoSession'
getWorkspaceSsoSession: 'getWorkspaceSsoSession',
getAdminOverrideEnabled: 'getAdminOverrideEnabled'
}
export const Loaders = AuthCheckContextLoaderKeys // shorter alias
/* v8 ignore end */
export type AuthCheckContextLoaderKeys =
@@ -63,6 +66,7 @@ export type AuthCheckContextLoaderKeys =
export type AllAuthCheckContextLoaders = AuthContextLoaderMappingDefinition<{
getEnv: GetEnv
getAdminOverrideEnabled: GetAdminOverrideEnabled
getProject: GetProject
getProjectRole: GetProjectRole
getServerRole: GetServerRole
+6 -1
View File
@@ -1,9 +1,14 @@
import Result from 'true-myth/result'
import Unit from 'true-myth/unit'
import { AuthError } from './authErrors.js'
import { AllAuthErrors, AuthError } from './authErrors.js'
import { AuthCheckContextLoaderKeys, AuthCheckContextLoaders } from './loaders.js'
import Maybe from 'true-myth/maybe'
export type AuthPolicyResult<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ExpectedAuthErrors extends AuthError<any, any> = AllAuthErrors
> = Result<Unit, ExpectedAuthErrors>
// a complete policy always returns a full result
export type AuthPolicy<
LoaderKeys extends AuthCheckContextLoaderKeys,
@@ -34,3 +34,5 @@ export type GetWorkspaceSsoSession = (
) => Promise<WorkspaceSsoSession | null>
export type GetEnv = () => Promise<FeatureFlags>
export type GetAdminOverrideEnabled = () => Promise<boolean>
@@ -1,12 +1,10 @@
import { describe, expect, it } from 'vitest'
import { maybeMemberRoleWithValidSsoSessionIfNeeded } from './workspaceSso.js'
import cryptoRandomString from 'crypto-random-string'
import { err, ok } from 'true-myth/result'
import {
WorkspaceNoAccessError,
WorkspaceSsoSessionNoAccessError
} from '../domain/authErrors.js'
import { just, nothing } from 'true-myth/maybe'
describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that', () => {
it('hides non existing workspaces behind a WorkspaceNoAccessError', async () => {
@@ -25,7 +23,9 @@ describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that',
userId: cryptoRandomString({ length: 10 }),
workspaceId: cryptoRandomString({ length: 10 })
})
await expect(result).resolves.toStrictEqual(just(err(new WorkspaceNoAccessError())))
await expect(result).resolves.toBeAuthErrorResult({
code: WorkspaceNoAccessError.code
})
})
it('returns WorkspaceNoAccessError if the user does not have a workspace role', async () => {
const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({
@@ -44,7 +44,9 @@ describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that',
userId: cryptoRandomString({ length: 10 }),
workspaceId: cryptoRandomString({ length: 10 })
})
expect(result).toStrictEqual(just(err(new WorkspaceNoAccessError())))
expect(result).toBeAuthErrorResult({
code: WorkspaceNoAccessError.code
})
})
it('returns nothing if user does not have a minimum workspace:member role', async () => {
const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({
@@ -63,7 +65,7 @@ describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that',
userId: cryptoRandomString({ length: 10 }),
workspaceId: cryptoRandomString({ length: 10 })
})
expect(result).toStrictEqual(nothing())
expect(result).toBeNothingResult()
})
it('returns just(ok()) if user is a member and workspace has no SSO provider', async () => {
const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({
@@ -80,7 +82,7 @@ describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that',
userId: cryptoRandomString({ length: 10 }),
workspaceId: cryptoRandomString({ length: 10 })
})
expect(result).toStrictEqual(just(ok()))
expect(result).toBeAuthOKResult()
})
it('returns WorkspaceSsoSessionInvalidError if user does not have an SSO session', async () => {
const result = maybeMemberRoleWithValidSsoSessionIfNeeded({
@@ -97,11 +99,11 @@ describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that',
userId: cryptoRandomString({ length: 10 }),
workspaceId: cryptoRandomString({ length: 10 })
})
await expect(result).resolves.toStrictEqual(
just(
err(new WorkspaceSsoSessionNoAccessError({ payload: { workspaceSlug: 'bbb' } }))
)
)
await expect(result).resolves.toBeAuthErrorResult({
code: WorkspaceSsoSessionNoAccessError.code,
payload: { workspaceSlug: 'bbb' }
})
})
it('returns WorkspaceSsoSessionInvalidError if user has an expired sso session', async () => {
const userId = cryptoRandomString({ length: 10 })
@@ -125,11 +127,11 @@ describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that',
userId,
workspaceId
})
expect(result).toStrictEqual(
just(
err(new WorkspaceSsoSessionNoAccessError({ payload: { workspaceSlug: 'bbb' } }))
)
)
expect(result).toBeAuthErrorResult({
code: WorkspaceSsoSessionNoAccessError.code,
payload: { workspaceSlug: 'bbb' }
})
})
it('returns true if user has a valid sso session', async () => {
const userId = cryptoRandomString({ length: 10 })
@@ -153,6 +155,6 @@ describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that',
userId,
workspaceId
})
expect(result).toStrictEqual(just(ok()))
expect(result).toBeAuthOKResult()
})
})
+1
View File
@@ -6,3 +6,4 @@ export {
} from './domain/loaders.js'
export * from './helpers/graphql.js'
export * from './domain/authErrors.js'
export { AuthPolicyResult } from './domain/policies.js'
+5 -3
View File
@@ -1,10 +1,12 @@
import { AllAuthCheckContextLoaders } from '../domain/loaders.js'
import { canCreateWorkspaceProjectPolicy } from './canCreateWorkspaceProject.js'
import { canReadProjectPolicy } from './canReadProject.js'
import { canCreateWorkspaceProjectPolicy } from './workspace/canCreateWorkspaceProject.js'
import { canReadProjectPolicy } from './project/canReadProject.js'
import { canCreateProjectPolicy } from './project/canCreate.js'
export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
project: {
canRead: canReadProjectPolicy(loaders)
canRead: canReadProjectPolicy(loaders),
canCreateLegacy: canCreateProjectPolicy(loaders)
},
workspace: {
canCreateProject: canCreateWorkspaceProjectPolicy(loaders)
@@ -0,0 +1,58 @@
import { describe, expect, it } from 'vitest'
import { canCreateProjectPolicy } from './canCreate.js'
import { parseFeatureFlags } from '../../../environment/index.js'
import {
ProjectNoAccessError,
ServerNoAccessError,
ServerNoSessionError
} from '../../domain/authErrors.js'
const buildSUT = (overrides?: Partial<Parameters<typeof canCreateProjectPolicy>[0]>) =>
canCreateProjectPolicy({
getEnv: async () =>
parseFeatureFlags({
FF_WORKSPACES_MODULE_ENABLED: 'false'
}),
getServerRole: async () => 'server:user',
...(overrides || {})
})
describe('canCreateProject', () => {
it('returns error if user is not logged in', async () => {
const canCreateProject = buildSUT()
const result = await canCreateProject({ userId: undefined })
expect(result).toBeAuthErrorResult({
code: ServerNoSessionError.code
})
})
// TODO: Re-enable when ready
it.skip('returns error if workspaces module is enabled', async () => {
const canCreateProject = buildSUT({
getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' })
})
const result = await canCreateProject({ userId: 'user-id' })
expect(result).toBeAuthErrorResult({
code: ProjectNoAccessError.code
})
})
it('returns error if user is a server guest', async () => {
const canCreateProject = buildSUT({
getServerRole: async () => 'server:guest'
})
const result = await canCreateProject({ userId: 'user-id' })
expect(result).toBeAuthErrorResult({
code: ServerNoAccessError.code
})
})
it('returns ok if user is a server user', async () => {
const canCreateProject = buildSUT()
const result = await canCreateProject({ userId: 'user-id' })
expect(result).toBeAuthOKResult()
})
})
@@ -0,0 +1,43 @@
import { err, ok } from 'true-myth/result'
import {
ProjectNoAccessError,
ServerNoAccessError,
ServerNoSessionError,
WorkspaceSsoSessionNoAccessError
} from '../../domain/authErrors.js'
import { MaybeUserContext } from '../../domain/context.js'
import { Loaders } from '../../domain/loaders.js'
import { AuthPolicy } from '../../domain/policies.js'
import { hasMinimumServerRole } from '../../checks/serverRole.js'
import { Roles } from '../../../core/constants.js'
export const canCreateProjectPolicy: AuthPolicy<
typeof Loaders.getServerRole | typeof Loaders.getEnv,
MaybeUserContext,
InstanceType<
| typeof ServerNoSessionError
| typeof ServerNoAccessError
| typeof ProjectNoAccessError
| typeof WorkspaceSsoSessionNoAccessError
>
> =
(loaders) =>
async ({ userId }) => {
const env = await loaders.getEnv()
if (!userId?.length) return err(new ServerNoSessionError())
if (env.FF_WORKSPACES_MODULE_ENABLED) {
// TODO: We're not ready to enforce this yet, there's a bunch of tests that would break
// return err(
// new ProjectNoAccessError({
// message: "Projects can't be created outside of workspaces"
// })
// )
}
const isActiveServerUser = await hasMinimumServerRole(loaders)({
userId,
role: Roles.Server.User
})
if (!isActiveServerUser) return err(new ServerNoAccessError())
return ok()
}
@@ -1,8 +1,8 @@
import { describe, expect, it, assert } from 'vitest'
import { canReadProjectPolicy } from './canReadProject.js'
import { parseFeatureFlags } from '../../environment/index.js'
import { parseFeatureFlags } from '../../../environment/index.js'
import crs from 'crypto-random-string'
import { Roles } from '../../core/constants.js'
import { Roles } from '../../../core/constants.js'
import {
ProjectNoAccessError,
ProjectNotFoundError,
@@ -10,11 +10,10 @@ import {
ServerNoSessionError,
WorkspaceNoAccessError,
WorkspaceSsoSessionNoAccessError
} from '../domain/authErrors.js'
import { getProjectFake } from '../../tests/fakes.js'
import { err, ok } from 'true-myth/result'
} from '../../domain/authErrors.js'
import { getProjectFake } from '../../../tests/fakes.js'
import cryptoRandomString from 'crypto-random-string'
import { AuthCheckContextLoaders } from '../domain/loaders.js'
import { AuthCheckContextLoaders } from '../../domain/loaders.js'
const canReadProjectArgs = () => {
const projectId = crs({ length: 10 })
@@ -32,6 +31,7 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
it('converts not found projects into ProjectNotFoundError', async () => {
const result = canReadProjectPolicy({
getWorkspace,
getAdminOverrideEnabled: async () => false,
getEnv: async () => parseFeatureFlags({}),
getProject: async () => null,
getProjectRole: () => {
@@ -51,12 +51,15 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
}
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(err(new ProjectNotFoundError()))
await expect(result).resolves.toBeAuthErrorResult({
code: ProjectNotFoundError.code
})
})
})
describe('project visibility', () => {
it('allows anyone on a public project', async () => {
const canReadProject = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () => parseFeatureFlags({}),
getProject: getProjectFake({ isPublic: true }),
getProjectRole: () => {
@@ -78,10 +81,11 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
}
})
const canQuery = await canReadProject(canReadProjectArgs())
expect(canQuery.isOk).toBe(true)
expect(canQuery).toBeAuthOKResult()
})
it('allows anyone on a linkShareable project', async () => {
const canReadProject = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () => parseFeatureFlags({}),
getProject: getProjectFake({ isDiscoverable: true }),
getProjectRole: () => {
@@ -102,13 +106,14 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
}
})
const canQuery = await canReadProject(canReadProjectArgs())
expect(canQuery.isOk).toBe(true)
expect(canQuery).toBeAuthOKResult()
})
})
describe('server roles', () => {
it('allows access for archived server users with a project role on a public project', async () => {
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
getProject: getProjectFake({ isDiscoverable: false, isPublic: true }),
@@ -125,10 +130,12 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
assert.fail()
}
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(ok())
await expect(result).resolves.toBeAuthOKResult()
})
it('does not allow access for archived server users with a project role', async () => {
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
@@ -145,10 +152,14 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
assert.fail()
}
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(err(new ServerNoAccessError()))
await expect(result).resolves.toBeAuthErrorResult({
code: ServerNoAccessError.code
})
})
it('does not allow access for non public projects for unknown users', async () => {
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
@@ -166,7 +177,10 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
assert.fail()
}
})({ userId: undefined, projectId: cryptoRandomString({ length: 10 }) })
await expect(result).resolves.toStrictEqual(err(new ServerNoSessionError()))
await expect(result).resolves.toBeAuthErrorResult({
code: ServerNoSessionError.code
})
})
})
@@ -175,6 +189,7 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
'allows access for active server users to private projects with %s role',
async (role) => {
const canReadProject = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
@@ -191,12 +206,14 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
assert.fail()
}
})
const canQuery = await canReadProject(canReadProjectArgs())
expect(canQuery.isOk).toBe(true)
expect(canQuery).toBeAuthOKResult()
}
)
it('does not allow access to private projects without a project role', async () => {
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
@@ -213,13 +230,17 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
assert.fail()
}
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(err(new ProjectNoAccessError()))
await expect(result).resolves.toBeAuthErrorResult({
code: ProjectNoAccessError.code
})
})
})
describe('admin override', () => {
it('allows server admins without project roles on private projects if admin override is enabled', async () => {
const result = canReadProjectPolicy({
getEnv: async () => parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'true' }),
getAdminOverrideEnabled: async () => true,
getEnv: async () => parseFeatureFlags({}),
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
getServerRole: async () => Roles.Server.Admin,
getProjectRole: () => {
@@ -236,14 +257,15 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
assert.fail()
}
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(ok())
await expect(result).resolves.toBeAuthOKResult()
})
it('does not allow server admins without project roles on private projects if admin override is disabled', async () => {
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({
FF_ADMIN_OVERRIDE_ENABLED: 'false',
FF_WORKSPACES_MODULE_ENABLED: 'false'
}),
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
@@ -261,12 +283,16 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
assert.fail()
}
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(err(new ProjectNoAccessError()))
await expect(result).resolves.toBeAuthErrorResult({
code: ProjectNoAccessError.code
})
})
})
describe('the workspace world', () => {
it('does not check workspace rules if the workspaces module is not enabled', async () => {
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
getProject: getProjectFake({
@@ -288,10 +314,12 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
assert.fail()
}
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(ok())
await expect(result).resolves.toBeAuthOKResult()
})
it('does not allow project access without a workspace role', async () => {
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({
FF_WORKSPACES_MODULE_ENABLED: 'true'
@@ -313,10 +341,14 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
assert.fail()
}
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(err(new WorkspaceNoAccessError()))
await expect(result).resolves.toBeAuthErrorResult({
code: WorkspaceNoAccessError.code
})
})
it('allows project access via workspace role if user does not have project role', async () => {
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({
FF_WORKSPACES_MODULE_ENABLED: 'true'
@@ -335,10 +367,12 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
getWorkspace,
getWorkspaceSsoProvider: async () => null
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(ok())
await expect(result).resolves.toBeAuthOKResult()
})
it('does not check SSO sessions if user is workspace guest', async () => {
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({
FF_WORKSPACES_MODULE_ENABLED: 'true'
@@ -359,10 +393,12 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
assert.fail()
}
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(ok())
await expect(result).resolves.toBeAuthOKResult()
})
it('does not check SSO sessions if workspace does not have it enabled', async () => {
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({
FF_WORKSPACES_MODULE_ENABLED: 'true'
@@ -381,10 +417,12 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
},
getWorkspaceSsoProvider: async () => null
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(ok())
await expect(result).resolves.toBeAuthOKResult()
})
it('does not allow project access if SSO session is missing', async () => {
const canReadProject = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({
FF_WORKSPACES_MODULE_ENABLED: 'true'
@@ -401,14 +439,18 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
getWorkspaceSsoSession: async () => null,
getWorkspaceSsoProvider: async () => ({ providerId: 'foo' })
})
const canQuery = await canReadProject(canReadProjectArgs())
expect(canQuery.isOk).toBe(false)
expect(canQuery).toBeAuthErrorResult({
code: WorkspaceSsoSessionNoAccessError.code
})
})
it('does not allow project access if SSO session is not found', async () => {
const date = new Date()
date.setDate(date.getDate() - 1)
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({
FF_WORKSPACES_MODULE_ENABLED: 'true'
@@ -425,19 +467,18 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
getWorkspaceSsoSession: async () => null,
getWorkspaceSsoProvider: async () => ({ providerId: 'foo' })
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(
err(
new WorkspaceSsoSessionNoAccessError({
payload: { workspaceSlug: 'bbb' }
})
)
)
await expect(result).resolves.toBeAuthErrorResult({
code: WorkspaceSsoSessionNoAccessError.code,
payload: { workspaceSlug: 'bbb' }
})
})
it('does not allow project access if SSO session is expired', async () => {
const date = new Date()
date.setDate(date.getDate() - 1)
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({
FF_WORKSPACES_MODULE_ENABLED: 'true'
@@ -458,19 +499,18 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
}),
getWorkspaceSsoProvider: async () => ({ providerId: 'foo' })
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(
err(
new WorkspaceSsoSessionNoAccessError({
payload: { workspaceSlug: 'bbb' }
})
)
)
await expect(result).resolves.toBeAuthErrorResult({
code: WorkspaceSsoSessionNoAccessError.code,
payload: { workspaceSlug: 'bbb' }
})
})
it('allows project access if SSO session is valid', async () => {
const date = new Date()
date.setDate(date.getDate() + 1)
const result = canReadProjectPolicy({
getAdminOverrideEnabled: async () => false,
getEnv: async () =>
parseFeatureFlags({
FF_WORKSPACES_MODULE_ENABLED: 'true'
@@ -491,7 +531,8 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
}),
getWorkspaceSsoProvider: async () => ({ providerId: 'foo' })
})(canReadProjectArgs())
await expect(result).resolves.toStrictEqual(ok())
await expect(result).resolves.toBeAuthOKResult()
})
})
})
@@ -1,5 +1,8 @@
import { Roles } from '../../core/constants.js'
import { hasMinimumProjectRole, isPubliclyReadableProject } from '../checks/projects.js'
import { Roles } from '../../../core/constants.js'
import {
hasMinimumProjectRole,
isPubliclyReadableProject
} from '../../checks/projects.js'
import {
ProjectNoAccessError,
ProjectNotFoundError,
@@ -7,16 +10,17 @@ import {
ServerNoSessionError,
WorkspaceNoAccessError,
WorkspaceSsoSessionNoAccessError
} from '../domain/authErrors.js'
} from '../../domain/authErrors.js'
import { err, ok } from 'true-myth/result'
import { AuthCheckContextLoaderKeys } from '../domain/loaders.js'
import { AuthPolicy } from '../domain/policies.js'
import { canUseAdminOverride, hasMinimumServerRole } from '../checks/serverRole.js'
import { hasAnyWorkspaceRole } from '../checks/workspaceRole.js'
import { maybeMemberRoleWithValidSsoSessionIfNeeded } from '../fragments/workspaceSso.js'
import { MaybeUserContext, ProjectContext } from '../domain/context.js'
import { AuthCheckContextLoaderKeys } from '../../domain/loaders.js'
import { AuthPolicy } from '../../domain/policies.js'
import { canUseAdminOverride, hasMinimumServerRole } from '../../checks/serverRole.js'
import { hasAnyWorkspaceRole } from '../../checks/workspaceRole.js'
import { maybeMemberRoleWithValidSsoSessionIfNeeded } from '../../fragments/workspaceSso.js'
import { MaybeUserContext, ProjectContext } from '../../domain/context.js'
export const canReadProjectPolicy: AuthPolicy<
| typeof AuthCheckContextLoaderKeys.getAdminOverrideEnabled
| typeof AuthCheckContextLoaderKeys.getEnv
| typeof AuthCheckContextLoaderKeys.getProject
| typeof AuthCheckContextLoaderKeys.getProjectRole
@@ -9,14 +9,13 @@ import {
WorkspaceReadOnlyError,
WorkspacesNotEnabledError,
WorkspaceSsoSessionNoAccessError
} from '../domain/authErrors.js'
} from '../../domain/authErrors.js'
import { nanoid } from 'nanoid'
import { canCreateWorkspaceProjectPolicy } from './canCreateWorkspaceProject.js'
import { parseFeatureFlags } from '../../environment/index.js'
import { parseFeatureFlags } from '../../../environment/index.js'
import cryptoRandomString from 'crypto-random-string'
import { WorkspacePlan } from '../../workspaces/index.js'
import { Workspace, WorkspaceSsoProvider } from '../domain/workspaces/types.js'
import { err, ok } from 'true-myth/result'
import { WorkspacePlan } from '../../../workspaces/index.js'
import { Workspace, WorkspaceSsoProvider } from '../../domain/workspaces/types.js'
const canCreateArgs = () => ({
userId: cryptoRandomString({ length: 10 }),
@@ -60,7 +59,9 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(err(new WorkspacesNotEnabledError()))
expect(result).toBeAuthErrorResult({
code: WorkspacesNotEnabledError.code
})
})
})
@@ -97,7 +98,9 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})({ workspaceId: '' })
expect(result).toStrictEqual(err(new ServerNoSessionError()))
expect(result).toBeAuthErrorResult({
code: ServerNoSessionError.code
})
})
it('forbids creation for anyone not having minimum server:user role', async () => {
const result = await canCreateWorkspaceProjectPolicy({
@@ -131,7 +134,9 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(err(new ServerNoAccessError()))
expect(result).toBeAuthErrorResult({
code: ServerNoAccessError.code
})
})
})
@@ -170,7 +175,9 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(err(new WorkspaceNoAccessError()))
expect(result).toBeAuthErrorResult({
code: WorkspaceNoAccessError.code
})
})
it('forbids creation when sso session is not found', async () => {
@@ -207,15 +214,12 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(
err(
new WorkspaceSsoSessionNoAccessError({
payload: { workspaceSlug }
})
)
)
expect(result).toBeAuthErrorResult({
code: WorkspaceSsoSessionNoAccessError.code,
payload: { workspaceSlug }
})
})
it('throws UncoveredError from unexpected sso session errors')
// it('throws UncoveredError from unexpected sso session errors')
})
describe('user workspace roles', () => {
@@ -251,7 +255,9 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(err(new WorkspaceNoAccessError()))
expect(result).toBeAuthErrorResult({
code: WorkspaceNoAccessError.code
})
})
it('WorkspaceNotEnoughPermissionsError for workspace guests', async () => {
const result = await canCreateWorkspaceProjectPolicy({
@@ -285,13 +291,10 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(
err(
new WorkspaceNotEnoughPermissionsError({
message: 'Guests cannot create projects in the workspace'
})
)
)
expect(result).toBeAuthErrorResult({
code: WorkspaceNotEnoughPermissionsError.code,
message: 'Guests cannot create projects in the workspace'
})
})
it('forbids non-editor seats from creating projects', async () => {
const result = await canCreateWorkspaceProjectPolicy({
@@ -328,7 +331,9 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(err(new WorkspaceNoEditorSeatError()))
expect(result).toBeAuthErrorResult({
code: WorkspaceNoEditorSeatError.code
})
})
})
@@ -365,7 +370,9 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(err(new WorkspaceNoAccessError()))
expect(result).toBeAuthErrorResult({
code: WorkspaceNoAccessError.code
})
})
it('forbids creation if plan is read-only', async () => {
const result = await canCreateWorkspaceProjectPolicy({
@@ -401,7 +408,9 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(err(new WorkspaceReadOnlyError()))
expect(result).toBeAuthErrorResult({
code: WorkspaceReadOnlyError.code
})
})
})
@@ -440,7 +449,9 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(err(new WorkspaceNoAccessError()))
expect(result).toBeAuthErrorResult({
code: WorkspaceNoAccessError.code
})
})
it('allows creation if plan has no limits', async () => {
const result = await canCreateWorkspaceProjectPolicy({
@@ -479,7 +490,7 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(ok())
expect(result).toBeAuthOKResult()
})
it('forbids creation if current project count fails to load', async () => {
const result = await canCreateWorkspaceProjectPolicy({
@@ -518,7 +529,9 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(err(new WorkspaceNoAccessError()))
expect(result).toBeAuthErrorResult({
code: WorkspaceNoAccessError.code
})
})
it('allows creation if new project is within plan limits', async () => {
const result = await canCreateWorkspaceProjectPolicy({
@@ -557,7 +570,7 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(ok())
expect(result).toBeAuthOKResult()
})
it('forbids creation if new project is not within plan limits', async () => {
const result = await canCreateWorkspaceProjectPolicy({
@@ -596,9 +609,10 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
}
})(canCreateArgs())
expect(result).toStrictEqual(
err(new WorkspaceLimitsReachedError({ payload: { limit: 'projectCount' } }))
)
expect(result).toBeAuthErrorResult({
code: WorkspaceLimitsReachedError.code,
payload: { limit: 'projectCount' }
})
})
})
})
@@ -1,4 +1,4 @@
import { AuthPolicy } from '../domain/policies.js'
import { AuthPolicy } from '../../domain/policies.js'
import {
ServerNoAccessError,
ServerNoSessionError,
@@ -9,18 +9,18 @@ import {
WorkspaceReadOnlyError,
WorkspacesNotEnabledError,
WorkspaceSsoSessionNoAccessError
} from '../domain/authErrors.js'
} from '../../domain/authErrors.js'
import { err, ok } from 'true-myth/result'
import { hasMinimumServerRole } from '../checks/serverRole.js'
import { Roles } from '../../core/constants.js'
import { maybeMemberRoleWithValidSsoSessionIfNeeded } from '../fragments/workspaceSso.js'
import { throwUncoveredError } from '../../core/index.js'
import { hasEditorSeat } from '../checks/workspaceSeat.js'
import { MaybeUserContext, WorkspaceContext } from '../domain/context.js'
import { hasMinimumServerRole } from '../../checks/serverRole.js'
import { Roles } from '../../../core/constants.js'
import { maybeMemberRoleWithValidSsoSessionIfNeeded } from '../../fragments/workspaceSso.js'
import { throwUncoveredError } from '../../../core/index.js'
import { hasEditorSeat } from '../../checks/workspaceSeat.js'
import { MaybeUserContext, WorkspaceContext } from '../../domain/context.js'
import {
isNewWorkspacePlan,
isWorkspacePlanStatusReadOnly
} from '../../workspaces/index.js'
} from '../../../workspaces/index.js'
export const canCreateWorkspaceProjectPolicy: AuthPolicy<
| 'getEnv'
-6
View File
@@ -13,11 +13,6 @@ export const parseFeatureFlags = (
//INFO
// As a convention all feature flags should be prefixed with a FF_
const res = parseEnv(input, {
// Enables the admin override feature
FF_ADMIN_OVERRIDE_ENABLED: {
schema: z.boolean(),
defaults: { production: false, _: false }
},
// Enables the automate module.
FF_AUTOMATE_MODULE_ENABLED: {
schema: z.boolean(),
@@ -99,7 +94,6 @@ export const parseFeatureFlags = (
let parsedFlags: FeatureFlags | undefined
export type FeatureFlags = {
FF_ADMIN_OVERRIDE_ENABLED: boolean
FF_AUTOMATE_MODULE_ENABLED: boolean
FF_GENDOAI_MODULE_ENABLED: boolean
FF_WORKSPACES_MODULE_ENABLED: boolean
+157
View File
@@ -0,0 +1,157 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-empty-object-type */
import { expect } from 'vitest'
import { AllAuthErrors, isAuthPolicyError } from '../authz/index.js'
import { isInstance as isResult } from 'true-myth/result'
import { isInstance as isMaybe } from 'true-myth/maybe'
// Augment vitest types w/ new matchers
interface CustomMatchers<R = unknown> {
toBeAuthOKResult: () => R
toBeAuthErrorResult: (params: {
/**
* Check for specific error code
*/
code?: AllAuthErrors['code']
/**
* Check for specific error message (includes not equals)
*/
message?: string
/**
* Check for a specific payload
*/
payload?: unknown
}) => R
toBeNothingResult: () => R
}
declare module 'vitest' {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}
// Extend w/ extra matchers
expect.extend({
toBeAuthOKResult(received: unknown) {
if (isMaybe(received) && received.isJust) {
received = received.value
}
if (!isResult(received)) {
return {
pass: false,
message: () => `Expected ${received} to be a Result structure`
}
}
if (!received.isOk) {
return {
pass: false,
message: () => `Expected ${received} to be an OK Result`
}
}
return {
pass: true,
message: () => `${received} is an OK Result`
}
},
toBeAuthErrorResult(
received: unknown,
expected: Parameters<CustomMatchers['toBeAuthErrorResult']>[0]
) {
const { code, message, payload } = expected
if (!code?.length && !message?.length && !payload) {
throw new Error(
'No expected value provided. Either code or message or payload must be set.'
)
}
if (isMaybe(received) && received.isJust) {
received = received.value
}
if (!isResult(received)) {
return {
pass: false,
message: () => `Expected ${received} to be a Result structure`
}
}
if (!received.isErr) {
return {
pass: false,
message: () => `Expected ${received} to be an Error Result`
}
}
if (!isAuthPolicyError(received.error)) {
return {
pass: false,
message: () => `Expected ${received} to be an Error Result with an AuthError`
}
}
// Sanity checks done, now do actual expectation checks
if (expected.code && received.error.code !== expected.code) {
return {
pass: false,
message: () =>
`Expected ${received} to be an Auth Error Result with code ${expected.code}`,
expected: expected.code,
actual: received.error.code
}
}
if (expected.message && !received.error.message.includes(expected.message)) {
return {
pass: false,
message: () =>
`Expected ${received} to be an Auth Error Result with message substring '${expected.message}'`,
expected: expected.message,
actual: received.error.message
}
}
if (expected.payload) {
const errPayload = received.error.payload
const equals = this.equals(errPayload, expected.payload)
if (!equals) {
return {
pass: false,
message: () =>
`Expected ${received} to be an Auth Error Result with payload ${expected.payload}`,
expected: expected.payload,
actual: errPayload
}
}
}
return {
pass: true,
message: () => `${received} is an Auth Error Result with code ${expected.code}`
}
},
toBeNothingResult(received: unknown) {
if (!isMaybe(received)) {
return {
pass: false,
message: () => `Expected ${received} to be a Maybe structure`
}
}
if (received.isJust) {
return {
pass: false,
message: () => `Expected ${received} to be a Nothing Result`
}
}
return {
pass: true,
message: () => `${received} is a Nothing Result`
}
}
})
+2 -1
View File
@@ -11,6 +11,7 @@ export default defineConfig({
enabled: true,
provider: 'v8',
include: ['src/**/*.{ts,tsx}']
}
},
setupFiles: ['./src/tests/setup.ts']
}
})
+224 -393
View File
@@ -10629,114 +10629,6 @@ __metadata:
languageName: node
linkType: hard
"@graphql-codegen/cli@npm:^5.0.2":
version: 5.0.2
resolution: "@graphql-codegen/cli@npm:5.0.2"
dependencies:
"@babel/generator": "npm:^7.18.13"
"@babel/template": "npm:^7.18.10"
"@babel/types": "npm:^7.18.13"
"@graphql-codegen/client-preset": "npm:^4.2.2"
"@graphql-codegen/core": "npm:^4.0.2"
"@graphql-codegen/plugin-helpers": "npm:^5.0.3"
"@graphql-tools/apollo-engine-loader": "npm:^8.0.0"
"@graphql-tools/code-file-loader": "npm:^8.0.0"
"@graphql-tools/git-loader": "npm:^8.0.0"
"@graphql-tools/github-loader": "npm:^8.0.0"
"@graphql-tools/graphql-file-loader": "npm:^8.0.0"
"@graphql-tools/json-file-loader": "npm:^8.0.0"
"@graphql-tools/load": "npm:^8.0.0"
"@graphql-tools/prisma-loader": "npm:^8.0.0"
"@graphql-tools/url-loader": "npm:^8.0.0"
"@graphql-tools/utils": "npm:^10.0.0"
"@whatwg-node/fetch": "npm:^0.8.0"
chalk: "npm:^4.1.0"
cosmiconfig: "npm:^8.1.3"
debounce: "npm:^1.2.0"
detect-indent: "npm:^6.0.0"
graphql-config: "npm:^5.0.2"
inquirer: "npm:^8.0.0"
is-glob: "npm:^4.0.1"
jiti: "npm:^1.17.1"
json-to-pretty-yaml: "npm:^1.2.2"
listr2: "npm:^4.0.5"
log-symbols: "npm:^4.0.0"
micromatch: "npm:^4.0.5"
shell-quote: "npm:^1.7.3"
string-env-interpolation: "npm:^1.0.1"
ts-log: "npm:^2.2.3"
tslib: "npm:^2.4.0"
yaml: "npm:^2.3.1"
yargs: "npm:^17.0.0"
peerDependencies:
"@parcel/watcher": ^2.1.0
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
peerDependenciesMeta:
"@parcel/watcher":
optional: true
bin:
gql-gen: cjs/bin.js
graphql-code-generator: cjs/bin.js
graphql-codegen: cjs/bin.js
graphql-codegen-esm: esm/bin.js
checksum: 10/24f5a4d441e4af2f0cae1818c8643a5400718cc1f08ca829a9110a35d99cb5529b567991ce826544b5a2aab36d0be3b10309dc112343bab1232d7c6f2fa14008
languageName: node
linkType: hard
"@graphql-codegen/cli@npm:^5.0.3":
version: 5.0.3
resolution: "@graphql-codegen/cli@npm:5.0.3"
dependencies:
"@babel/generator": "npm:^7.18.13"
"@babel/template": "npm:^7.18.10"
"@babel/types": "npm:^7.18.13"
"@graphql-codegen/client-preset": "npm:^4.4.0"
"@graphql-codegen/core": "npm:^4.0.2"
"@graphql-codegen/plugin-helpers": "npm:^5.0.3"
"@graphql-tools/apollo-engine-loader": "npm:^8.0.0"
"@graphql-tools/code-file-loader": "npm:^8.0.0"
"@graphql-tools/git-loader": "npm:^8.0.0"
"@graphql-tools/github-loader": "npm:^8.0.0"
"@graphql-tools/graphql-file-loader": "npm:^8.0.0"
"@graphql-tools/json-file-loader": "npm:^8.0.0"
"@graphql-tools/load": "npm:^8.0.0"
"@graphql-tools/prisma-loader": "npm:^8.0.0"
"@graphql-tools/url-loader": "npm:^8.0.0"
"@graphql-tools/utils": "npm:^10.0.0"
"@whatwg-node/fetch": "npm:^0.9.20"
chalk: "npm:^4.1.0"
cosmiconfig: "npm:^8.1.3"
debounce: "npm:^1.2.0"
detect-indent: "npm:^6.0.0"
graphql-config: "npm:^5.1.1"
inquirer: "npm:^8.0.0"
is-glob: "npm:^4.0.1"
jiti: "npm:^1.17.1"
json-to-pretty-yaml: "npm:^1.2.2"
listr2: "npm:^4.0.5"
log-symbols: "npm:^4.0.0"
micromatch: "npm:^4.0.5"
shell-quote: "npm:^1.7.3"
string-env-interpolation: "npm:^1.0.1"
ts-log: "npm:^2.2.3"
tslib: "npm:^2.4.0"
yaml: "npm:^2.3.1"
yargs: "npm:^17.0.0"
peerDependencies:
"@parcel/watcher": ^2.1.0
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
peerDependenciesMeta:
"@parcel/watcher":
optional: true
bin:
gql-gen: cjs/bin.js
graphql-code-generator: cjs/bin.js
graphql-codegen: cjs/bin.js
graphql-codegen-esm: esm/bin.js
checksum: 10/c3359668f824246e78656d26af506b5b279d50e08a56f54db87da492bd4d0a8e8b6540a6119402d7f5026c137babfd79e628897c6038e199ee6322f688eec757
languageName: node
linkType: hard
"@graphql-codegen/cli@npm:^5.0.5":
version: 5.0.5
resolution: "@graphql-codegen/cli@npm:5.0.5"
@@ -10791,7 +10683,7 @@ __metadata:
languageName: node
linkType: hard
"@graphql-codegen/client-preset@npm:^4.2.2, @graphql-codegen/client-preset@npm:^4.3.0":
"@graphql-codegen/client-preset@npm:^4.3.0":
version: 4.3.0
resolution: "@graphql-codegen/client-preset@npm:4.3.0"
dependencies:
@@ -10814,29 +10706,6 @@ __metadata:
languageName: node
linkType: hard
"@graphql-codegen/client-preset@npm:^4.4.0":
version: 4.5.0
resolution: "@graphql-codegen/client-preset@npm:4.5.0"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.20.2"
"@babel/template": "npm:^7.20.7"
"@graphql-codegen/add": "npm:^5.0.3"
"@graphql-codegen/gql-tag-operations": "npm:4.0.11"
"@graphql-codegen/plugin-helpers": "npm:^5.1.0"
"@graphql-codegen/typed-document-node": "npm:^5.0.11"
"@graphql-codegen/typescript": "npm:^4.1.1"
"@graphql-codegen/typescript-operations": "npm:^4.3.1"
"@graphql-codegen/visitor-plugin-common": "npm:^5.5.0"
"@graphql-tools/documents": "npm:^1.0.0"
"@graphql-tools/utils": "npm:^10.0.0"
"@graphql-typed-document-node/core": "npm:3.2.0"
tslib: "npm:~2.6.0"
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
checksum: 10/bbbbaa255f6cb1248cd143b54e06f6fc553cdd9f7ca002977bbf42b92cf9d5c6fe052eda1ae1233eab3d50dd80fbb04609bfeeb29132019faead04300e61ddc0
languageName: node
linkType: hard
"@graphql-codegen/client-preset@npm:^4.6.0, @graphql-codegen/client-preset@npm:^4.6.4":
version: 4.6.4
resolution: "@graphql-codegen/client-preset@npm:4.6.4"
@@ -10874,21 +10743,6 @@ __metadata:
languageName: node
linkType: hard
"@graphql-codegen/gql-tag-operations@npm:4.0.11":
version: 4.0.11
resolution: "@graphql-codegen/gql-tag-operations@npm:4.0.11"
dependencies:
"@graphql-codegen/plugin-helpers": "npm:^5.1.0"
"@graphql-codegen/visitor-plugin-common": "npm:5.5.0"
"@graphql-tools/utils": "npm:^10.0.0"
auto-bind: "npm:~4.0.0"
tslib: "npm:~2.6.0"
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
checksum: 10/cc277d1af9da611dbd37c00f18d08e8fdc634632c0fba6789a1027931f8e3b925ad64af27a6fa7c23ed44afdef131f9c03025ca9b077cd6e95e5c9823751c6a3
languageName: node
linkType: hard
"@graphql-codegen/gql-tag-operations@npm:4.0.16":
version: 4.0.16
resolution: "@graphql-codegen/gql-tag-operations@npm:4.0.16"
@@ -10964,21 +10818,6 @@ __metadata:
languageName: node
linkType: hard
"@graphql-codegen/typed-document-node@npm:^5.0.11":
version: 5.0.11
resolution: "@graphql-codegen/typed-document-node@npm:5.0.11"
dependencies:
"@graphql-codegen/plugin-helpers": "npm:^5.1.0"
"@graphql-codegen/visitor-plugin-common": "npm:5.5.0"
auto-bind: "npm:~4.0.0"
change-case-all: "npm:1.0.15"
tslib: "npm:~2.6.0"
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
checksum: 10/9320fbc9ccf13d0b0ecc7b57f1b0799629ce93a4e0cf95a76cdeb38981e2da92775734daa7bf68a9383e3d01f9a47f4b35cb870aef710f5dc137234b93b9d7cf
languageName: node
linkType: hard
"@graphql-codegen/typed-document-node@npm:^5.0.15":
version: 5.0.15
resolution: "@graphql-codegen/typed-document-node@npm:5.0.15"
@@ -11009,6 +10848,21 @@ __metadata:
languageName: node
linkType: hard
"@graphql-codegen/typed-document-node@npm:^5.1.1":
version: 5.1.1
resolution: "@graphql-codegen/typed-document-node@npm:5.1.1"
dependencies:
"@graphql-codegen/plugin-helpers": "npm:^5.1.0"
"@graphql-codegen/visitor-plugin-common": "npm:5.8.0"
auto-bind: "npm:~4.0.0"
change-case-all: "npm:1.0.15"
tslib: "npm:~2.6.0"
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
checksum: 10/213983a6c10173dc61041f331c19d6152a7e1bb07ab10c70355d49a7048a367ccee30428eda652b514c32431f938606b1db5c04f0d2c9dd93abb0096187cb91a
languageName: node
linkType: hard
"@graphql-codegen/typescript-operations@npm:^4.2.1":
version: 4.2.1
resolution: "@graphql-codegen/typescript-operations@npm:4.2.1"
@@ -11024,21 +10878,6 @@ __metadata:
languageName: node
linkType: hard
"@graphql-codegen/typescript-operations@npm:^4.3.1":
version: 4.3.1
resolution: "@graphql-codegen/typescript-operations@npm:4.3.1"
dependencies:
"@graphql-codegen/plugin-helpers": "npm:^5.1.0"
"@graphql-codegen/typescript": "npm:^4.1.1"
"@graphql-codegen/visitor-plugin-common": "npm:5.5.0"
auto-bind: "npm:~4.0.0"
tslib: "npm:~2.6.0"
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
checksum: 10/cdad24e16aa9b369e3ef2434032f2527fd1363e82256dd09d2e9aa6d9a55539eeea15665a4289e7695145f7417a9a765ad73979054a97c606d757ee060780819
languageName: node
linkType: hard
"@graphql-codegen/typescript-operations@npm:^4.5.1":
version: 4.5.1
resolution: "@graphql-codegen/typescript-operations@npm:4.5.1"
@@ -11054,19 +10893,36 @@ __metadata:
languageName: node
linkType: hard
"@graphql-codegen/typescript-resolvers@npm:^4.4.0":
version: 4.4.0
resolution: "@graphql-codegen/typescript-resolvers@npm:4.4.0"
"@graphql-codegen/typescript-operations@npm:^4.6.0":
version: 4.6.0
resolution: "@graphql-codegen/typescript-operations@npm:4.6.0"
dependencies:
"@graphql-codegen/plugin-helpers": "npm:^5.1.0"
"@graphql-codegen/typescript": "npm:^4.1.1"
"@graphql-codegen/visitor-plugin-common": "npm:5.5.0"
"@graphql-codegen/typescript": "npm:^4.1.6"
"@graphql-codegen/visitor-plugin-common": "npm:5.8.0"
auto-bind: "npm:~4.0.0"
tslib: "npm:~2.6.0"
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
graphql-sock: ^1.0.0
checksum: 10/6b835dce1db8f73f9f1d51ff8258f1cccbd40618a3582c923eb9ee1761a481502beaea719869d96741e5dd3a4cc2bb87c4fd0f6ab369f7dc22fbc838f6e7751f
languageName: node
linkType: hard
"@graphql-codegen/typescript-resolvers@npm:^4.5.0":
version: 4.5.0
resolution: "@graphql-codegen/typescript-resolvers@npm:4.5.0"
dependencies:
"@graphql-codegen/plugin-helpers": "npm:^5.1.0"
"@graphql-codegen/typescript": "npm:^4.1.6"
"@graphql-codegen/visitor-plugin-common": "npm:5.8.0"
"@graphql-tools/utils": "npm:^10.0.0"
auto-bind: "npm:~4.0.0"
tslib: "npm:~2.6.0"
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
checksum: 10/26efe707c89a9612da0ff8be56ebdb91227fe6afb3889b22f49bbec2cd12ba677d58f7946871179dc64c8279acbad773987086fd1c388980c9a7932fd7c15e32
graphql-sock: ^1.0.0
checksum: 10/02986bacf81da793a3501c5bbf810a0eb53b56cf42fbb760f62e8d1a9a6d929a35dff22d01bc1120a075f23a6ded35a6fd14d0c4cbb3d49effe7b4ecb9234ad4
languageName: node
linkType: hard
@@ -11085,21 +10941,6 @@ __metadata:
languageName: node
linkType: hard
"@graphql-codegen/typescript@npm:^4.1.1":
version: 4.1.1
resolution: "@graphql-codegen/typescript@npm:4.1.1"
dependencies:
"@graphql-codegen/plugin-helpers": "npm:^5.1.0"
"@graphql-codegen/schema-ast": "npm:^4.0.2"
"@graphql-codegen/visitor-plugin-common": "npm:5.5.0"
auto-bind: "npm:~4.0.0"
tslib: "npm:~2.6.0"
peerDependencies:
graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
checksum: 10/a47fabef00832122f4981fecbbcfd1e90e2567bdc7fc1d63520b018ae1a6db5217eb42f4f4744265cc492e64cd134b87b7bcfdaddfd7b3e35ce5c47d4548225d
languageName: node
linkType: hard
"@graphql-codegen/typescript@npm:^4.1.5":
version: 4.1.5
resolution: "@graphql-codegen/typescript@npm:4.1.5"
@@ -11115,6 +10956,21 @@ __metadata:
languageName: node
linkType: hard
"@graphql-codegen/typescript@npm:^4.1.6":
version: 4.1.6
resolution: "@graphql-codegen/typescript@npm:4.1.6"
dependencies:
"@graphql-codegen/plugin-helpers": "npm:^5.1.0"
"@graphql-codegen/schema-ast": "npm:^4.0.2"
"@graphql-codegen/visitor-plugin-common": "npm:5.8.0"
auto-bind: "npm:~4.0.0"
tslib: "npm:~2.6.0"
peerDependencies:
graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
checksum: 10/a32804685743fb5561d397515c9ddc92031f97712add706ad7ca38c628c3a52f9455f0880a425a8bdc8377c4ee39f80c8be2097fefeaa499b19c03e4e6abb584
languageName: node
linkType: hard
"@graphql-codegen/visitor-plugin-common@npm:5.2.0, @graphql-codegen/visitor-plugin-common@npm:^5.2.0":
version: 5.2.0
resolution: "@graphql-codegen/visitor-plugin-common@npm:5.2.0"
@@ -11135,26 +10991,6 @@ __metadata:
languageName: node
linkType: hard
"@graphql-codegen/visitor-plugin-common@npm:5.5.0, @graphql-codegen/visitor-plugin-common@npm:^5.5.0":
version: 5.5.0
resolution: "@graphql-codegen/visitor-plugin-common@npm:5.5.0"
dependencies:
"@graphql-codegen/plugin-helpers": "npm:^5.1.0"
"@graphql-tools/optimize": "npm:^2.0.0"
"@graphql-tools/relay-operation-optimizer": "npm:^7.0.0"
"@graphql-tools/utils": "npm:^10.0.0"
auto-bind: "npm:~4.0.0"
change-case-all: "npm:1.0.15"
dependency-graph: "npm:^0.11.0"
graphql-tag: "npm:^2.11.0"
parse-filepath: "npm:^1.0.2"
tslib: "npm:~2.6.0"
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
checksum: 10/f923c40ae996a2accf3a951d302b3da9b3c063f4b1c66b159bf3f74910e18ea592e87b3f35495a84f6c36d1198d880dd07f6e8c3fe94b0d6dba0f2f77522cb5d
languageName: node
linkType: hard
"@graphql-codegen/visitor-plugin-common@npm:5.7.1, @graphql-codegen/visitor-plugin-common@npm:^5.7.1":
version: 5.7.1
resolution: "@graphql-codegen/visitor-plugin-common@npm:5.7.1"
@@ -11175,6 +11011,26 @@ __metadata:
languageName: node
linkType: hard
"@graphql-codegen/visitor-plugin-common@npm:5.8.0":
version: 5.8.0
resolution: "@graphql-codegen/visitor-plugin-common@npm:5.8.0"
dependencies:
"@graphql-codegen/plugin-helpers": "npm:^5.1.0"
"@graphql-tools/optimize": "npm:^2.0.0"
"@graphql-tools/relay-operation-optimizer": "npm:^7.0.0"
"@graphql-tools/utils": "npm:^10.0.0"
auto-bind: "npm:~4.0.0"
change-case-all: "npm:1.0.15"
dependency-graph: "npm:^0.11.0"
graphql-tag: "npm:^2.11.0"
parse-filepath: "npm:^1.0.2"
tslib: "npm:~2.6.0"
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
checksum: 10/6d9c5e40d3dcda1f1c209440be5a7530581eac2ec00f787f3e45218bba024a5eeda8f2cb8811e05c6d69d114df0097f2377cbfceedb7786fb0e558d4511904e4
languageName: node
linkType: hard
"@graphql-tools/apollo-engine-loader@npm:^8.0.0":
version: 8.0.1
resolution: "@graphql-tools/apollo-engine-loader@npm:8.0.1"
@@ -14112,6 +13968,13 @@ __metadata:
languageName: node
linkType: hard
"@parcel/watcher-android-arm64@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-android-arm64@npm:2.5.1"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@parcel/watcher-darwin-arm64@npm:2.4.1":
version: 2.4.1
resolution: "@parcel/watcher-darwin-arm64@npm:2.4.1"
@@ -14119,6 +13982,13 @@ __metadata:
languageName: node
linkType: hard
"@parcel/watcher-darwin-arm64@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-darwin-arm64@npm:2.5.1"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@parcel/watcher-darwin-x64@npm:2.4.1":
version: 2.4.1
resolution: "@parcel/watcher-darwin-x64@npm:2.4.1"
@@ -14126,6 +13996,13 @@ __metadata:
languageName: node
linkType: hard
"@parcel/watcher-darwin-x64@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-darwin-x64@npm:2.5.1"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@parcel/watcher-freebsd-x64@npm:2.4.1":
version: 2.4.1
resolution: "@parcel/watcher-freebsd-x64@npm:2.4.1"
@@ -14133,6 +14010,13 @@ __metadata:
languageName: node
linkType: hard
"@parcel/watcher-freebsd-x64@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-freebsd-x64@npm:2.5.1"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"@parcel/watcher-linux-arm-glibc@npm:2.4.1":
version: 2.4.1
resolution: "@parcel/watcher-linux-arm-glibc@npm:2.4.1"
@@ -14140,6 +14024,20 @@ __metadata:
languageName: node
linkType: hard
"@parcel/watcher-linux-arm-glibc@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-linux-arm-glibc@npm:2.5.1"
conditions: os=linux & cpu=arm & libc=glibc
languageName: node
linkType: hard
"@parcel/watcher-linux-arm-musl@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-linux-arm-musl@npm:2.5.1"
conditions: os=linux & cpu=arm & libc=musl
languageName: node
linkType: hard
"@parcel/watcher-linux-arm64-glibc@npm:2.4.1":
version: 2.4.1
resolution: "@parcel/watcher-linux-arm64-glibc@npm:2.4.1"
@@ -14147,6 +14045,13 @@ __metadata:
languageName: node
linkType: hard
"@parcel/watcher-linux-arm64-glibc@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-linux-arm64-glibc@npm:2.5.1"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@parcel/watcher-linux-arm64-musl@npm:2.4.1":
version: 2.4.1
resolution: "@parcel/watcher-linux-arm64-musl@npm:2.4.1"
@@ -14154,6 +14059,13 @@ __metadata:
languageName: node
linkType: hard
"@parcel/watcher-linux-arm64-musl@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-linux-arm64-musl@npm:2.5.1"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@parcel/watcher-linux-x64-glibc@npm:2.4.1":
version: 2.4.1
resolution: "@parcel/watcher-linux-x64-glibc@npm:2.4.1"
@@ -14161,6 +14073,13 @@ __metadata:
languageName: node
linkType: hard
"@parcel/watcher-linux-x64-glibc@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-linux-x64-glibc@npm:2.5.1"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@parcel/watcher-linux-x64-musl@npm:2.4.1":
version: 2.4.1
resolution: "@parcel/watcher-linux-x64-musl@npm:2.4.1"
@@ -14168,6 +14087,13 @@ __metadata:
languageName: node
linkType: hard
"@parcel/watcher-linux-x64-musl@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-linux-x64-musl@npm:2.5.1"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@parcel/watcher-wasm@npm:^2.4.1":
version: 2.4.1
resolution: "@parcel/watcher-wasm@npm:2.4.1"
@@ -14186,6 +14112,13 @@ __metadata:
languageName: node
linkType: hard
"@parcel/watcher-win32-arm64@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-win32-arm64@npm:2.5.1"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@parcel/watcher-win32-ia32@npm:2.4.1":
version: 2.4.1
resolution: "@parcel/watcher-win32-ia32@npm:2.4.1"
@@ -14193,6 +14126,13 @@ __metadata:
languageName: node
linkType: hard
"@parcel/watcher-win32-ia32@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-win32-ia32@npm:2.5.1"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@parcel/watcher-win32-x64@npm:2.4.1":
version: 2.4.1
resolution: "@parcel/watcher-win32-x64@npm:2.4.1"
@@ -14200,6 +14140,13 @@ __metadata:
languageName: node
linkType: hard
"@parcel/watcher-win32-x64@npm:2.5.1":
version: 2.5.1
resolution: "@parcel/watcher-win32-x64@npm:2.5.1"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@parcel/watcher@npm:^2.4.1":
version: 2.4.1
resolution: "@parcel/watcher@npm:2.4.1"
@@ -14250,36 +14197,56 @@ __metadata:
languageName: node
linkType: hard
"@peculiar/asn1-schema@npm:^2.1.6":
version: 2.2.0
resolution: "@peculiar/asn1-schema@npm:2.2.0"
"@parcel/watcher@npm:^2.5.1":
version: 2.5.1
resolution: "@parcel/watcher@npm:2.5.1"
dependencies:
asn1js: "npm:^3.0.5"
pvtsutils: "npm:^1.3.2"
tslib: "npm:^2.4.0"
checksum: 10/1f467f387b0a1efa709377e0ed8c5b562f7bc3b6aa5dcf4df9a2ab20bba464b20a05c71870be802b77edc0df881b89427ad6afcd4196176fdbffdafd1fdd7484
languageName: node
linkType: hard
"@peculiar/json-schema@npm:^1.1.12":
version: 1.1.12
resolution: "@peculiar/json-schema@npm:1.1.12"
dependencies:
tslib: "npm:^2.0.0"
checksum: 10/dfec178afe63a02b6d45da8a18e51ef417e9f5412a8c2809c9a07b29b9376fadee1b4f2ea2d92d4e5a7b8eba76d9e99afbef6d7e9a27bd85257f69c4da228cbc
languageName: node
linkType: hard
"@peculiar/webcrypto@npm:^1.4.0":
version: 1.4.0
resolution: "@peculiar/webcrypto@npm:1.4.0"
dependencies:
"@peculiar/asn1-schema": "npm:^2.1.6"
"@peculiar/json-schema": "npm:^1.1.12"
pvtsutils: "npm:^1.3.2"
tslib: "npm:^2.4.0"
webcrypto-core: "npm:^1.7.4"
checksum: 10/125596cdc92c1b5aad1486c503e108648f7654912da8b73484857bb81b8c9ca1e03833b4fdc8d797a7b40f1107d129a6c7d541fd67ab9d8dd4d146d528ea0126
"@parcel/watcher-android-arm64": "npm:2.5.1"
"@parcel/watcher-darwin-arm64": "npm:2.5.1"
"@parcel/watcher-darwin-x64": "npm:2.5.1"
"@parcel/watcher-freebsd-x64": "npm:2.5.1"
"@parcel/watcher-linux-arm-glibc": "npm:2.5.1"
"@parcel/watcher-linux-arm-musl": "npm:2.5.1"
"@parcel/watcher-linux-arm64-glibc": "npm:2.5.1"
"@parcel/watcher-linux-arm64-musl": "npm:2.5.1"
"@parcel/watcher-linux-x64-glibc": "npm:2.5.1"
"@parcel/watcher-linux-x64-musl": "npm:2.5.1"
"@parcel/watcher-win32-arm64": "npm:2.5.1"
"@parcel/watcher-win32-ia32": "npm:2.5.1"
"@parcel/watcher-win32-x64": "npm:2.5.1"
detect-libc: "npm:^1.0.3"
is-glob: "npm:^4.0.3"
micromatch: "npm:^4.0.5"
node-addon-api: "npm:^7.0.0"
node-gyp: "npm:latest"
dependenciesMeta:
"@parcel/watcher-android-arm64":
optional: true
"@parcel/watcher-darwin-arm64":
optional: true
"@parcel/watcher-darwin-x64":
optional: true
"@parcel/watcher-freebsd-x64":
optional: true
"@parcel/watcher-linux-arm-glibc":
optional: true
"@parcel/watcher-linux-arm-musl":
optional: true
"@parcel/watcher-linux-arm64-glibc":
optional: true
"@parcel/watcher-linux-arm64-musl":
optional: true
"@parcel/watcher-linux-x64-glibc":
optional: true
"@parcel/watcher-linux-x64-musl":
optional: true
"@parcel/watcher-win32-arm64":
optional: true
"@parcel/watcher-win32-ia32":
optional: true
"@parcel/watcher-win32-x64":
optional: true
checksum: 10/2cc1405166fb3016b34508661902ab08b6dec59513708165c633c84a4696fff64f9b99ea116e747c121215e09619f1decab6f0350d1cb26c9210b98eb28a6a56
languageName: node
linkType: hard
@@ -16452,13 +16419,13 @@ __metadata:
resolution: "@speckle/dui3@workspace:packages/dui3"
dependencies:
"@apollo/client": "npm:^3.7.14"
"@graphql-codegen/cli": "npm:^5.0.2"
"@graphql-codegen/cli": "npm:^5.0.5"
"@graphql-codegen/client-preset": "npm:^4.3.0"
"@headlessui/vue": "npm:^1.7.13"
"@heroicons/vue": "npm:^2.0.12"
"@nuxt/eslint": "npm:^0.3.13"
"@nuxtjs/tailwindcss": "npm:^6.7.0"
"@parcel/watcher": "npm:^2.4.1"
"@parcel/watcher": "npm:^2.5.1"
"@pinia/nuxt": "npm:^0.4.11"
"@speckle/shared": "workspace:^"
"@speckle/ui-components": "workspace:^"
@@ -16564,7 +16531,7 @@ __metadata:
"@nuxt/eslint": "npm:^1.1.0"
"@nuxt/image": "npm:^1.8.1"
"@nuxtjs/tailwindcss": "npm:^6.12.2"
"@parcel/watcher": "npm:^2.4.1"
"@parcel/watcher": "npm:^2.5.1"
"@speckle/shared": "workspace:^"
"@speckle/tailwind-theme": "workspace:^"
"@speckle/ui-components": "workspace:^"
@@ -16837,11 +16804,11 @@ __metadata:
"@bull-board/express": "npm:^4.2.2"
"@faker-js/faker": "npm:^8.4.1"
"@godaddy/terminus": "npm:^4.9.0"
"@graphql-codegen/cli": "npm:^5.0.3"
"@graphql-codegen/typed-document-node": "npm:^5.0.11"
"@graphql-codegen/typescript": "npm:^4.1.1"
"@graphql-codegen/typescript-operations": "npm:^4.3.1"
"@graphql-codegen/typescript-resolvers": "npm:^4.4.0"
"@graphql-codegen/cli": "npm:^5.0.5"
"@graphql-codegen/typed-document-node": "npm:^5.1.1"
"@graphql-codegen/typescript": "npm:^4.1.6"
"@graphql-codegen/typescript-operations": "npm:^4.6.0"
"@graphql-codegen/typescript-resolvers": "npm:^4.5.0"
"@graphql-tools/mock": "npm:^9.0.4"
"@graphql-tools/schema": "npm:^10.0.6"
"@isaacs/ttlcache": "npm:^1.4.1"
@@ -16855,7 +16822,7 @@ __metadata:
"@opentelemetry/instrumentation-knex": "npm:^0.40.0"
"@opentelemetry/instrumentation-pino": "npm:^0.42.0"
"@opentelemetry/sdk-trace-node": "npm:^1.26.0"
"@parcel/watcher": "npm:^2.4.1"
"@parcel/watcher": "npm:^2.5.1"
"@speckle/objectloader": "workspace:^"
"@speckle/shared": "workspace:^"
"@swc/core": "npm:^1.11.11"
@@ -22463,13 +22430,6 @@ __metadata:
languageName: node
linkType: hard
"@whatwg-node/events@npm:^0.0.3":
version: 0.0.3
resolution: "@whatwg-node/events@npm:0.0.3"
checksum: 10/af26f40d4d0a0f5f0ee45fc6124afb8d6b33988dae96ab0fb87aa5e66d1ff08a749491b9da533ea524bbaebd4a770736f254d574a91ab4455386aa098cee8c77
languageName: node
linkType: hard
"@whatwg-node/events@npm:^0.1.0":
version: 0.1.1
resolution: "@whatwg-node/events@npm:0.1.1"
@@ -22487,19 +22447,6 @@ __metadata:
languageName: node
linkType: hard
"@whatwg-node/fetch@npm:^0.8.0":
version: 0.8.8
resolution: "@whatwg-node/fetch@npm:0.8.8"
dependencies:
"@peculiar/webcrypto": "npm:^1.4.0"
"@whatwg-node/node-fetch": "npm:^0.3.6"
busboy: "npm:^1.6.0"
urlpattern-polyfill: "npm:^8.0.0"
web-streams-polyfill: "npm:^3.2.1"
checksum: 10/4d04f28a3db1886a5ab6070af0d8d6b90c891596495e62417aa296dcdf65506703fb5f76937f7a7b7f4125721ef80f4ac9204a948588c33517dc064c746d7a42
languageName: node
linkType: hard
"@whatwg-node/fetch@npm:^0.9.0":
version: 0.9.18
resolution: "@whatwg-node/fetch@npm:0.9.18"
@@ -22510,29 +22457,6 @@ __metadata:
languageName: node
linkType: hard
"@whatwg-node/fetch@npm:^0.9.20":
version: 0.9.23
resolution: "@whatwg-node/fetch@npm:0.9.23"
dependencies:
"@whatwg-node/node-fetch": "npm:^0.6.0"
urlpattern-polyfill: "npm:^10.0.0"
checksum: 10/6024a3fcc2175de6a20ea4833c009d0488cf68c01cd235541ec0dba0ce59bb0b0befcd4cd788db0e65b99a5a8755bc00d490dc9d7beeb0c2f35058ef46732fe0
languageName: node
linkType: hard
"@whatwg-node/node-fetch@npm:^0.3.6":
version: 0.3.6
resolution: "@whatwg-node/node-fetch@npm:0.3.6"
dependencies:
"@whatwg-node/events": "npm:^0.0.3"
busboy: "npm:^1.6.0"
fast-querystring: "npm:^1.1.1"
fast-url-parser: "npm:^1.1.3"
tslib: "npm:^2.3.1"
checksum: 10/8284e385cf50f4479f19a5be8feb0d55f448af3bb7a62ec654ec9e4232ce3f0858191494f508f5196a94b16017d5e08f8e0bce9f49af4dc133a39d5047b8e369
languageName: node
linkType: hard
"@whatwg-node/node-fetch@npm:^0.5.7":
version: 0.5.11
resolution: "@whatwg-node/node-fetch@npm:0.5.11"
@@ -22546,18 +22470,6 @@ __metadata:
languageName: node
linkType: hard
"@whatwg-node/node-fetch@npm:^0.6.0":
version: 0.6.0
resolution: "@whatwg-node/node-fetch@npm:0.6.0"
dependencies:
"@kamilkisiela/fast-url-parser": "npm:^1.1.4"
busboy: "npm:^1.6.0"
fast-querystring: "npm:^1.1.1"
tslib: "npm:^2.6.3"
checksum: 10/87ad7c4cc68b24499089166617d16cbe25d9107b4d9354c804232f8c53c4fc27d1e2166471d878390442620e09588aa1d8705a8e2ea5bcc2d728a558ad1156c3
languageName: node
linkType: hard
"@whatwg-node/node-fetch@npm:^0.7.11":
version: 0.7.11
resolution: "@whatwg-node/node-fetch@npm:0.7.11"
@@ -23625,17 +23537,6 @@ __metadata:
languageName: node
linkType: hard
"asn1js@npm:^3.0.1, asn1js@npm:^3.0.5":
version: 3.0.5
resolution: "asn1js@npm:3.0.5"
dependencies:
pvtsutils: "npm:^1.3.2"
pvutils: "npm:^1.1.3"
tslib: "npm:^2.4.0"
checksum: 10/17fb0302432186631550de9606a4622ec366646d072cde9cdf4bcafa47bd2425e157eeb7b1377ee6520f8b46687b4ecaee31cf0ad2fa494361a1938b2ed53194
languageName: node
linkType: hard
"assert-never@npm:^1.2.1":
version: 1.2.1
resolution: "assert-never@npm:1.2.1"
@@ -33220,31 +33121,6 @@ __metadata:
languageName: node
linkType: hard
"graphql-config@npm:^5.0.2":
version: 5.0.3
resolution: "graphql-config@npm:5.0.3"
dependencies:
"@graphql-tools/graphql-file-loader": "npm:^8.0.0"
"@graphql-tools/json-file-loader": "npm:^8.0.0"
"@graphql-tools/load": "npm:^8.0.0"
"@graphql-tools/merge": "npm:^9.0.0"
"@graphql-tools/url-loader": "npm:^8.0.0"
"@graphql-tools/utils": "npm:^10.0.0"
cosmiconfig: "npm:^8.1.0"
jiti: "npm:^1.18.2"
minimatch: "npm:^4.2.3"
string-env-interpolation: "npm:^1.0.1"
tslib: "npm:^2.4.0"
peerDependencies:
cosmiconfig-toml-loader: ^1.0.0
graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
peerDependenciesMeta:
cosmiconfig-toml-loader:
optional: true
checksum: 10/be7667ea260c7db3e8b02c0d73d2a2bc214683d91886f883c73465e07aa204f9ae6bff494eaa253def31abd2bbe59e78c6b418ed456e06d2274050dbc45e33e7
languageName: node
linkType: hard
"graphql-config@npm:^5.1.1":
version: 5.1.3
resolution: "graphql-config@npm:5.1.3"
@@ -38912,15 +38788,6 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^4.2.3":
version: 4.2.3
resolution: "minimatch@npm:4.2.3"
dependencies:
brace-expansion: "npm:^1.1.7"
checksum: 10/02bacb187448c6aeeed5a794a64799fb1d1dd0274694da97272a9d3b919c7dfba6987d2089f6465191450d36d767c47fd7958227610b919121ccaed7de7f8924
languageName: node
linkType: hard
"minimatch@npm:^5.1.0":
version: 5.1.0
resolution: "minimatch@npm:5.1.0"
@@ -45333,22 +45200,6 @@ __metadata:
languageName: node
linkType: hard
"pvtsutils@npm:^1.3.2":
version: 1.3.2
resolution: "pvtsutils@npm:1.3.2"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10/3e89fea1836dd9027446d65923f7240372a1040b777b2e6adfc319bfeb3cacfd56dccb708652651e85ad6a5c87f61728b697226c105d441140b648f3e4167872
languageName: node
linkType: hard
"pvutils@npm:^1.1.3":
version: 1.1.3
resolution: "pvutils@npm:1.1.3"
checksum: 10/e5201b8f78ece68eae414a938c844bc45fb3f0de298178eed1775a217eedfd897c4346e5e54f410bb4d7466e09ceb262e85f20fd64239b8bb2595f14c52fa95e
languageName: node
linkType: hard
"qs@npm:6.11.0, qs@npm:^6.10.0":
version: 6.11.0
resolution: "qs@npm:6.11.0"
@@ -52051,7 +51902,7 @@ __metadata:
languageName: node
linkType: hard
"urlpattern-polyfill@npm:8.0.2, urlpattern-polyfill@npm:^8.0.0":
"urlpattern-polyfill@npm:8.0.2":
version: 8.0.2
resolution: "urlpattern-polyfill@npm:8.0.2"
checksum: 10/fd86b5c55473f3abbf9ed317b953c9cbb4fa6b3f75f681a1d982fe9c17bbc8d9bcf988f4cf3bda35e2e5875984086c97e177f97f076bb80dfa2beb85d1dd7b23
@@ -53418,26 +53269,6 @@ __metadata:
languageName: node
linkType: hard
"web-streams-polyfill@npm:^3.2.1":
version: 3.3.3
resolution: "web-streams-polyfill@npm:3.3.3"
checksum: 10/8e7e13501b3834094a50abe7c0b6456155a55d7571312b89570012ef47ec2a46d766934768c50aabad10a9c30dd764a407623e8bfcc74fcb58495c29130edea9
languageName: node
linkType: hard
"webcrypto-core@npm:^1.7.4":
version: 1.7.5
resolution: "webcrypto-core@npm:1.7.5"
dependencies:
"@peculiar/asn1-schema": "npm:^2.1.6"
"@peculiar/json-schema": "npm:^1.1.12"
asn1js: "npm:^3.0.1"
pvtsutils: "npm:^1.3.2"
tslib: "npm:^2.4.0"
checksum: 10/f322c6ec494102bb0ad8d242915e7d838341f4555b6d9940c0686fd492a5a1f3ecb78825c4d4e75a1136677dac2e96f138acece22ef8f9f5d36c0e88a9f3a20f
languageName: node
linkType: hard
"webgl-sdf-generator@npm:1.1.1":
version: 1.1.1
resolution: "webgl-sdf-generator@npm:1.1.1"