feat(server): workspace roles taken into account in project queries (#4319)
* Workspace.projects fixed * Query.project tested & fixed * personalOnly flag added * withProjectRoleOnly flag * authorizeResolver implicit workspace roles * minor cleanup * reorg + support for throwing auth errors * global error mapping * undo special borkage * CR fixes * more CR fixes * shared tests fix * minor adjustment * tests fix * see if removing cached roles fixes it? * more fixes * clean up debugging garbage
This commit is contained in:
committed by
GitHub
parent
e3d3c1446b
commit
820a1e2ebf
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -466,7 +466,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,
|
||||
@@ -492,8 +492,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,
|
||||
@@ -536,7 +535,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,
|
||||
@@ -610,6 +609,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,
|
||||
@@ -1037,7 +1037,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.
|
||||
*/
|
||||
@@ -1141,11 +1141,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.
|
||||
*/
|
||||
@@ -1317,7 +1313,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.
|
||||
*/
|
||||
@@ -1610,6 +1606,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.
|
||||
*/
|
||||
|
||||
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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -4076,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']>;
|
||||
};
|
||||
|
||||
@@ -4861,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 = {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
])
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -698,7 +698,8 @@ const getUserStreamsQueryBaseFactory =
|
||||
withRoles,
|
||||
streamIdWhitelist,
|
||||
workspaceId,
|
||||
onlyWithActiveSsoSession
|
||||
onlyWithActiveSsoSession,
|
||||
personalOnly
|
||||
}: BaseUserStreamsQueryParams) => {
|
||||
const query = tables
|
||||
.streamAcl(deps.db)
|
||||
@@ -733,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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -4056,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']>;
|
||||
};
|
||||
|
||||
@@ -4841,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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -241,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 = {
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
revokeStreamPermissionsFactory,
|
||||
grantStreamPermissionsFactory,
|
||||
legacyGetStreamsFactory,
|
||||
getUserStreamsPageFactory,
|
||||
getUserStreamsCountFactory,
|
||||
getStreamCollaboratorsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { InviteCreateValidationError } from '@/modules/serverinvites/errors'
|
||||
@@ -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()
|
||||
@@ -1207,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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import { orderByWeight } from '@/modules/shared/domain/rolesAndScopes/logic'
|
||||
import coreUserRoles from '@/modules/core/roles'
|
||||
import {
|
||||
GetStreamCollaborators,
|
||||
GetUserStreamsPage,
|
||||
LegacyGetStreams
|
||||
} from '@/modules/core/domain/streams/operations'
|
||||
import { ProjectNotFoundError } from '@/modules/core/errors/projects'
|
||||
@@ -87,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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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'
|
||||
])
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -4057,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']>;
|
||||
};
|
||||
|
||||
@@ -4842,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 = {
|
||||
@@ -6067,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; }>;
|
||||
|
||||
@@ -6249,6 +6258,6 @@ export const GetWorkspaceProjectsDocument = {"kind":"Document","definitions":[{"
|
||||
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>;
|
||||
@@ -230,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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
+80
-39
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
+13
-9
@@ -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
|
||||
+49
-35
@@ -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' }
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
+9
-9
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
||||
enabled: true,
|
||||
provider: 'v8',
|
||||
include: ['src/**/*.{ts,tsx}']
|
||||
}
|
||||
},
|
||||
setupFiles: ['./src/tests/setup.ts']
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user