feat(workspaces): emit who will be added to workspace for a given project move (#4332)
* wip * feat(workspaces): preflight service wip * feat(workspaces): move project to workspace dry run * fix(workspaces): add tests and refine query * chore(workspaces): gqlgen
This commit is contained in:
@@ -2008,6 +2008,8 @@ export type Project = {
|
||||
* real or fake (e.g., with a foo/bar model, it will be nested under foo even if such a model doesn't actually exist)
|
||||
*/
|
||||
modelsTree: ModelsTreeItemCollection;
|
||||
/** Returns information about the potential effects of moving a project to a given workspace. */
|
||||
moveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRun;
|
||||
name: Scalars['String']['output'];
|
||||
object?: Maybe<Object>;
|
||||
/** Pending project access requests */
|
||||
@@ -2106,6 +2108,11 @@ export type ProjectModelsTreeArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMoveToWorkspaceDryRunArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectObjectArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
@@ -2426,6 +2433,17 @@ export const ProjectModelsUpdatedMessageType = {
|
||||
} as const;
|
||||
|
||||
export type ProjectModelsUpdatedMessageType = typeof ProjectModelsUpdatedMessageType[keyof typeof ProjectModelsUpdatedMessageType];
|
||||
export type ProjectMoveToWorkspaceDryRun = {
|
||||
__typename?: 'ProjectMoveToWorkspaceDryRun';
|
||||
addedToWorkspace: Array<LimitedUser>;
|
||||
addedToWorkspaceTotalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMoveToWorkspaceDryRunAddedToWorkspaceArgs = {
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export type ProjectMutations = {
|
||||
__typename?: 'ProjectMutations';
|
||||
/** Access request related mutations */
|
||||
@@ -7364,6 +7382,7 @@ export type AllObjectTypes = {
|
||||
ProjectFileImportUpdatedMessage: ProjectFileImportUpdatedMessage,
|
||||
ProjectInviteMutations: ProjectInviteMutations,
|
||||
ProjectModelsUpdatedMessage: ProjectModelsUpdatedMessage,
|
||||
ProjectMoveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRun,
|
||||
ProjectMutations: ProjectMutations,
|
||||
ProjectPendingModelsUpdatedMessage: ProjectPendingModelsUpdatedMessage,
|
||||
ProjectPendingVersionsUpdatedMessage: ProjectPendingVersionsUpdatedMessage,
|
||||
@@ -8039,6 +8058,7 @@ export type ProjectFieldArgs = {
|
||||
modelChildrenTree: ProjectModelChildrenTreeArgs,
|
||||
models: ProjectModelsArgs,
|
||||
modelsTree: ProjectModelsTreeArgs,
|
||||
moveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRunArgs,
|
||||
name: {},
|
||||
object: ProjectObjectArgs,
|
||||
pendingAccessRequests: {},
|
||||
@@ -8121,6 +8141,10 @@ export type ProjectModelsUpdatedMessageFieldArgs = {
|
||||
model: {},
|
||||
type: {},
|
||||
}
|
||||
export type ProjectMoveToWorkspaceDryRunFieldArgs = {
|
||||
addedToWorkspace: ProjectMoveToWorkspaceDryRunAddedToWorkspaceArgs,
|
||||
addedToWorkspaceTotalCount: {},
|
||||
}
|
||||
export type ProjectMutationsFieldArgs = {
|
||||
accessRequestMutations: {},
|
||||
automationMutations: ProjectMutationsAutomationMutationsArgs,
|
||||
@@ -8868,6 +8892,7 @@ export type AllObjectFieldArgTypes = {
|
||||
ProjectFileImportUpdatedMessage: ProjectFileImportUpdatedMessageFieldArgs,
|
||||
ProjectInviteMutations: ProjectInviteMutationsFieldArgs,
|
||||
ProjectModelsUpdatedMessage: ProjectModelsUpdatedMessageFieldArgs,
|
||||
ProjectMoveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRunFieldArgs,
|
||||
ProjectMutations: ProjectMutationsFieldArgs,
|
||||
ProjectPendingModelsUpdatedMessage: ProjectPendingModelsUpdatedMessageFieldArgs,
|
||||
ProjectPendingVersionsUpdatedMessage: ProjectPendingVersionsUpdatedMessageFieldArgs,
|
||||
|
||||
@@ -550,6 +550,15 @@ extend type User {
|
||||
|
||||
extend type Project {
|
||||
workspace: Workspace
|
||||
"""
|
||||
Returns information about the potential effects of moving a project to a given workspace.
|
||||
"""
|
||||
moveToWorkspaceDryRun(workspaceId: String!): ProjectMoveToWorkspaceDryRun!
|
||||
}
|
||||
|
||||
type ProjectMoveToWorkspaceDryRun {
|
||||
addedToWorkspace(limit: Int): [LimitedUser!]!
|
||||
addedToWorkspaceTotalCount: Int!
|
||||
}
|
||||
|
||||
type ServerWorkspacesInfo {
|
||||
|
||||
@@ -72,6 +72,7 @@ generates:
|
||||
WorkspaceSubscriptionSeats: '@/modules/gatekeeper/helpers/graphTypes#WorkspaceSubscriptionSeatsGraphQLReturn'
|
||||
WorkspaceJoinRequest: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceJoinRequestGraphQLReturn'
|
||||
LimitedWorkspaceJoinRequest: '@/modules/workspacesCore/helpers/graphTypes#LimitedWorkspaceJoinRequestGraphQLReturn'
|
||||
ProjectMoveToWorkspaceDryRun: '@/modules/workspacesCore/helpers/graphTypes#ProjectMoveToWorkspaceDryRunGraphQLReturn'
|
||||
Webhook: '@/modules/webhooks/helpers/graphTypes#WebhookGraphQLReturn'
|
||||
SmartTextEditorValue: '@/modules/core/services/richTextEditorService#SmartTextEditorValueGraphQLReturn'
|
||||
BlobMetadata: '@/modules/blobstorage/domain/types#BlobStorageItem'
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { ProjectTeamMember } from '@/modules/core/domain/projects/types'
|
||||
import { Project } from '@/modules/core/domain/streams/types'
|
||||
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
|
||||
import { MaybeNullOrUndefined, StreamRoles } from '@speckle/shared'
|
||||
|
||||
export type GetProject = (args: { projectId: string }) => Promise<Project | null>
|
||||
|
||||
export type GetProjectCollaborators = (args: {
|
||||
projectId: string
|
||||
}) => Promise<ProjectTeamMember[]>
|
||||
|
||||
export type UpdateProject = (args: {
|
||||
projectUpdate: Pick<StreamRecord, 'id' | 'workspaceId'>
|
||||
}) => Promise<StreamRecord>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn } from
|
||||
import { PendingStreamCollaboratorGraphQLReturn } from '@/modules/serverinvites/helpers/graphTypes';
|
||||
import { FileUploadGraphQLReturn } from '@/modules/fileuploads/helpers/types';
|
||||
import { AutomateFunctionGraphQLReturn, AutomateFunctionReleaseGraphQLReturn, AutomationGraphQLReturn, AutomationRevisionGraphQLReturn, AutomationRevisionFunctionGraphQLReturn, AutomateRunGraphQLReturn, AutomationRunTriggerGraphQLReturn, AutomationRevisionTriggerDefinitionGraphQLReturn, AutomateFunctionRunGraphQLReturn, TriggeredAutomationsStatusGraphQLReturn, ProjectAutomationMutationsGraphQLReturn, ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn, ProjectAutomationsUpdatedMessageGraphQLReturn, UserAutomateInfoGraphQLReturn } from '@/modules/automate/helpers/graphTypes';
|
||||
import { WorkspaceGraphQLReturn, WorkspaceSsoGraphQLReturn, WorkspaceMutationsGraphQLReturn, WorkspaceJoinRequestMutationsGraphQLReturn, WorkspaceInviteMutationsGraphQLReturn, WorkspaceProjectMutationsGraphQLReturn, PendingWorkspaceCollaboratorGraphQLReturn, WorkspaceCollaboratorGraphQLReturn, WorkspaceJoinRequestGraphQLReturn, LimitedWorkspaceJoinRequestGraphQLReturn, ProjectRoleGraphQLReturn, WorkspacePermissionChecksGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes';
|
||||
import { WorkspaceGraphQLReturn, WorkspaceSsoGraphQLReturn, WorkspaceMutationsGraphQLReturn, WorkspaceJoinRequestMutationsGraphQLReturn, WorkspaceInviteMutationsGraphQLReturn, WorkspaceProjectMutationsGraphQLReturn, PendingWorkspaceCollaboratorGraphQLReturn, WorkspaceCollaboratorGraphQLReturn, WorkspaceJoinRequestGraphQLReturn, LimitedWorkspaceJoinRequestGraphQLReturn, ProjectMoveToWorkspaceDryRunGraphQLReturn, ProjectRoleGraphQLReturn, WorkspacePermissionChecksGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes';
|
||||
import { WorkspacePlanGraphQLReturn, WorkspacePlanUsageGraphQLReturn, PriceGraphQLReturn } from '@/modules/gatekeeperCore/helpers/graphTypes';
|
||||
import { WorkspaceBillingMutationsGraphQLReturn, WorkspaceSubscriptionSeatsGraphQLReturn, WorkspaceSubscriptionGraphQLReturn } from '@/modules/gatekeeper/helpers/graphTypes';
|
||||
import { WebhookGraphQLReturn } from '@/modules/webhooks/helpers/graphTypes';
|
||||
@@ -2031,6 +2031,8 @@ export type Project = {
|
||||
* real or fake (e.g., with a foo/bar model, it will be nested under foo even if such a model doesn't actually exist)
|
||||
*/
|
||||
modelsTree: ModelsTreeItemCollection;
|
||||
/** Returns information about the potential effects of moving a project to a given workspace. */
|
||||
moveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRun;
|
||||
name: Scalars['String']['output'];
|
||||
object?: Maybe<Object>;
|
||||
/** Pending project access requests */
|
||||
@@ -2129,6 +2131,11 @@ export type ProjectModelsTreeArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMoveToWorkspaceDryRunArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectObjectArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
@@ -2449,6 +2456,17 @@ export const ProjectModelsUpdatedMessageType = {
|
||||
} as const;
|
||||
|
||||
export type ProjectModelsUpdatedMessageType = typeof ProjectModelsUpdatedMessageType[keyof typeof ProjectModelsUpdatedMessageType];
|
||||
export type ProjectMoveToWorkspaceDryRun = {
|
||||
__typename?: 'ProjectMoveToWorkspaceDryRun';
|
||||
addedToWorkspace: Array<LimitedUser>;
|
||||
addedToWorkspaceTotalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMoveToWorkspaceDryRunAddedToWorkspaceArgs = {
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export type ProjectMutations = {
|
||||
__typename?: 'ProjectMutations';
|
||||
/** Access request related mutations */
|
||||
@@ -5226,6 +5244,7 @@ export type ResolversTypes = {
|
||||
ProjectModelsTreeFilter: ProjectModelsTreeFilter;
|
||||
ProjectModelsUpdatedMessage: ResolverTypeWrapper<Omit<ProjectModelsUpdatedMessage, 'model'> & { model?: Maybe<ResolversTypes['Model']> }>;
|
||||
ProjectModelsUpdatedMessageType: ProjectModelsUpdatedMessageType;
|
||||
ProjectMoveToWorkspaceDryRun: ResolverTypeWrapper<ProjectMoveToWorkspaceDryRunGraphQLReturn>;
|
||||
ProjectMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
|
||||
ProjectPendingModelsUpdatedMessage: ResolverTypeWrapper<Omit<ProjectPendingModelsUpdatedMessage, 'model'> & { model: ResolversTypes['FileUpload'] }>;
|
||||
ProjectPendingModelsUpdatedMessageType: ProjectPendingModelsUpdatedMessageType;
|
||||
@@ -5545,6 +5564,7 @@ export type ResolversParentTypes = {
|
||||
ProjectModelsFilter: ProjectModelsFilter;
|
||||
ProjectModelsTreeFilter: ProjectModelsTreeFilter;
|
||||
ProjectModelsUpdatedMessage: Omit<ProjectModelsUpdatedMessage, 'model'> & { model?: Maybe<ResolversParentTypes['Model']> };
|
||||
ProjectMoveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRunGraphQLReturn;
|
||||
ProjectMutations: MutationsObjectGraphQLReturn;
|
||||
ProjectPendingModelsUpdatedMessage: Omit<ProjectPendingModelsUpdatedMessage, 'model'> & { model: ResolversParentTypes['FileUpload'] };
|
||||
ProjectPendingVersionsUpdatedMessage: Omit<ProjectPendingVersionsUpdatedMessage, 'version'> & { version: ResolversParentTypes['FileUpload'] };
|
||||
@@ -6458,6 +6478,7 @@ export type ProjectResolvers<ContextType = GraphQLContext, ParentType extends Re
|
||||
modelChildrenTree?: Resolver<Array<ResolversTypes['ModelsTreeItem']>, ParentType, ContextType, RequireFields<ProjectModelChildrenTreeArgs, 'fullName'>>;
|
||||
models?: Resolver<ResolversTypes['ModelCollection'], ParentType, ContextType, RequireFields<ProjectModelsArgs, 'limit'>>;
|
||||
modelsTree?: Resolver<ResolversTypes['ModelsTreeItemCollection'], ParentType, ContextType, RequireFields<ProjectModelsTreeArgs, 'limit'>>;
|
||||
moveToWorkspaceDryRun?: Resolver<ResolversTypes['ProjectMoveToWorkspaceDryRun'], ParentType, ContextType, RequireFields<ProjectMoveToWorkspaceDryRunArgs, 'workspaceId'>>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
object?: Resolver<Maybe<ResolversTypes['Object']>, ParentType, ContextType, RequireFields<ProjectObjectArgs, 'id'>>;
|
||||
pendingAccessRequests?: Resolver<Maybe<Array<ResolversTypes['ProjectAccessRequest']>>, ParentType, ContextType>;
|
||||
@@ -6564,6 +6585,12 @@ export type ProjectModelsUpdatedMessageResolvers<ContextType = GraphQLContext, P
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type ProjectMoveToWorkspaceDryRunResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ProjectMoveToWorkspaceDryRun'] = ResolversParentTypes['ProjectMoveToWorkspaceDryRun']> = {
|
||||
addedToWorkspace?: Resolver<Array<ResolversTypes['LimitedUser']>, ParentType, ContextType, Partial<ProjectMoveToWorkspaceDryRunAddedToWorkspaceArgs>>;
|
||||
addedToWorkspaceTotalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type ProjectMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ProjectMutations'] = ResolversParentTypes['ProjectMutations']> = {
|
||||
accessRequestMutations?: Resolver<ResolversTypes['ProjectAccessRequestMutations'], ParentType, ContextType>;
|
||||
automationMutations?: Resolver<ResolversTypes['ProjectAutomationMutations'], ParentType, ContextType, RequireFields<ProjectMutationsAutomationMutationsArgs, 'projectId'>>;
|
||||
@@ -7492,6 +7519,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
ProjectFileImportUpdatedMessage?: ProjectFileImportUpdatedMessageResolvers<ContextType>;
|
||||
ProjectInviteMutations?: ProjectInviteMutationsResolvers<ContextType>;
|
||||
ProjectModelsUpdatedMessage?: ProjectModelsUpdatedMessageResolvers<ContextType>;
|
||||
ProjectMoveToWorkspaceDryRun?: ProjectMoveToWorkspaceDryRunResolvers<ContextType>;
|
||||
ProjectMutations?: ProjectMutationsResolvers<ContextType>;
|
||||
ProjectPendingModelsUpdatedMessage?: ProjectPendingModelsUpdatedMessageResolvers<ContextType>;
|
||||
ProjectPendingVersionsUpdatedMessage?: ProjectPendingVersionsUpdatedMessageResolvers<ContextType>;
|
||||
|
||||
@@ -57,7 +57,6 @@ import { metaHelpers } from '@/modules/core/helpers/meta'
|
||||
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
|
||||
import {
|
||||
DeleteProjectRole,
|
||||
GetProjectCollaborators,
|
||||
UpdateProject,
|
||||
GetRolesByUserId,
|
||||
UpsertProjectRole,
|
||||
@@ -686,12 +685,6 @@ export const getStreamCollaboratorsFactory =
|
||||
return items
|
||||
}
|
||||
|
||||
export const getProjectCollaboratorsFactory =
|
||||
(deps: { db: Knex }): GetProjectCollaborators =>
|
||||
async ({ projectId }) => {
|
||||
return await getStreamCollaboratorsFactory(deps)(projectId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base query for finding or counting user streams
|
||||
*/
|
||||
|
||||
@@ -2011,6 +2011,8 @@ export type Project = {
|
||||
* real or fake (e.g., with a foo/bar model, it will be nested under foo even if such a model doesn't actually exist)
|
||||
*/
|
||||
modelsTree: ModelsTreeItemCollection;
|
||||
/** Returns information about the potential effects of moving a project to a given workspace. */
|
||||
moveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRun;
|
||||
name: Scalars['String']['output'];
|
||||
object?: Maybe<Object>;
|
||||
/** Pending project access requests */
|
||||
@@ -2109,6 +2111,11 @@ export type ProjectModelsTreeArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMoveToWorkspaceDryRunArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectObjectArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
@@ -2429,6 +2436,17 @@ export const ProjectModelsUpdatedMessageType = {
|
||||
} as const;
|
||||
|
||||
export type ProjectModelsUpdatedMessageType = typeof ProjectModelsUpdatedMessageType[keyof typeof ProjectModelsUpdatedMessageType];
|
||||
export type ProjectMoveToWorkspaceDryRun = {
|
||||
__typename?: 'ProjectMoveToWorkspaceDryRun';
|
||||
addedToWorkspace: Array<LimitedUser>;
|
||||
addedToWorkspaceTotalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMoveToWorkspaceDryRunAddedToWorkspaceArgs = {
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export type ProjectMutations = {
|
||||
__typename?: 'ProjectMutations';
|
||||
/** Access request related mutations */
|
||||
|
||||
@@ -26,6 +26,7 @@ import { TokenResourceIdentifier } from '@/modules/core/domain/tokens/types'
|
||||
import { ServerRegion } from '@/modules/multiregion/domain/types'
|
||||
import { SetOptional } from 'type-fest'
|
||||
import { WorkspaceSeat, WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing'
|
||||
import { UserRecord } from '@/modules/core/helpers/userHelper'
|
||||
|
||||
/** Workspace */
|
||||
|
||||
@@ -445,3 +446,8 @@ export type SetUserActiveWorkspace = (args: {
|
||||
/** Is the user in a "personal project" outside of a workspace? */
|
||||
isProjectsActive?: boolean
|
||||
}) => Promise<void>
|
||||
|
||||
export type IntersectProjectCollaboratorsAndWorkspaceCollaborators = (params: {
|
||||
projectId: string
|
||||
workspaceId: string
|
||||
}) => Promise<UserRecord[]>
|
||||
|
||||
@@ -3,10 +3,12 @@ import { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { getPaginatedItemsFactory } from '@/modules/shared/services/paginatedItems'
|
||||
import { WorkspaceTeamMember } from '@/modules/workspaces/domain/types'
|
||||
import { intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory } from '@/modules/workspaces/repositories/projects'
|
||||
import {
|
||||
countInvitableCollaboratorsByProjectIdFactory,
|
||||
getInvitableCollaboratorsByProjectIdFactory
|
||||
} from '@/modules/workspaces/repositories/users'
|
||||
import { getMoveProjectToWorkspaceDryRunFactory } from '@/modules/workspaces/services/projects'
|
||||
|
||||
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
||||
|
||||
@@ -46,6 +48,25 @@ export default FF_WORKSPACES_MODULE_ENABLED
|
||||
cursor: args.cursor ?? undefined,
|
||||
limit: args.limit
|
||||
})
|
||||
},
|
||||
moveToWorkspaceDryRun: async (parent, args) => {
|
||||
const { id: projectId } = parent
|
||||
const { workspaceId } = args
|
||||
|
||||
const { addedToWorkspace } = await getMoveProjectToWorkspaceDryRunFactory({
|
||||
intersectProjectCollaboratorsAndWorkspaceCollaborators:
|
||||
intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory({ db })
|
||||
})({ projectId, workspaceId })
|
||||
|
||||
return addedToWorkspace
|
||||
}
|
||||
},
|
||||
ProjectMoveToWorkspaceDryRun: {
|
||||
addedToWorkspace: async (parent, args) => {
|
||||
return args.limit ? parent.slice(0, args.limit) : parent
|
||||
},
|
||||
addedToWorkspaceTotalCount: async (parent) => {
|
||||
return parent.length
|
||||
}
|
||||
}
|
||||
} as Resolvers)
|
||||
|
||||
@@ -2,7 +2,6 @@ import { db } from '@/db/knex'
|
||||
import { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
|
||||
import {
|
||||
getProjectCollaboratorsFactory,
|
||||
updateProjectFactory,
|
||||
upsertProjectRoleFactory,
|
||||
getRolesByUserIdFactory,
|
||||
@@ -12,7 +11,8 @@ import {
|
||||
grantStreamPermissionsFactory,
|
||||
legacyGetStreamsFactory,
|
||||
getUserStreamsPageFactory,
|
||||
getUserStreamsCountFactory
|
||||
getUserStreamsCountFactory,
|
||||
getStreamCollaboratorsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { InviteCreateValidationError } from '@/modules/serverinvites/errors'
|
||||
import {
|
||||
@@ -1027,7 +1027,7 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
getProject: getProjectFactory({ db }),
|
||||
updateProject: updateProjectFactory({ db }),
|
||||
upsertProjectRole: upsertProjectRoleFactory({ db }),
|
||||
getProjectCollaborators: getProjectCollaboratorsFactory({ db }),
|
||||
getProjectCollaborators: getStreamCollaboratorsFactory({ db }),
|
||||
getWorkspaceRolesAndSeats: getWorkspaceRolesAndSeatsFactory({ db }),
|
||||
updateWorkspaceRole: updateWorkspaceRoleFactory({
|
||||
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
|
||||
@@ -1519,7 +1519,7 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
getTotalCount: getWorkspaceCollaboratorsTotalCountFactory({ db })
|
||||
})({
|
||||
workspaceId: parent.id,
|
||||
limit: args.limit,
|
||||
limit: args.limit ?? 100,
|
||||
cursor: args.cursor ?? undefined
|
||||
})
|
||||
return team
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { StreamAcl, Users } from '@/modules/core/dbSchema'
|
||||
import { StreamAclRecord } from '@/modules/core/helpers/types'
|
||||
import { UserRecord } from '@/modules/core/helpers/userHelper'
|
||||
import { IntersectProjectCollaboratorsAndWorkspaceCollaborators } from '@/modules/workspaces/domain/operations'
|
||||
import { WorkspaceAcl } from '@/modules/workspacesCore/helpers/db'
|
||||
import { Knex } from 'knex'
|
||||
|
||||
const tables = {
|
||||
streamAcl: (db: Knex) => db.table<StreamAclRecord>('stream_acl')
|
||||
}
|
||||
|
||||
export const intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory =
|
||||
(deps: { db: Knex }): IntersectProjectCollaboratorsAndWorkspaceCollaborators =>
|
||||
async ({ projectId, workspaceId }) => {
|
||||
return await tables
|
||||
.streamAcl(deps.db)
|
||||
.select<UserRecord[]>(...Users.cols)
|
||||
.join(Users.name, Users.col.id, StreamAcl.col.userId)
|
||||
.where(StreamAcl.col.resourceId, projectId)
|
||||
.except((builder) => {
|
||||
return builder
|
||||
.select(...Users.cols)
|
||||
.from(WorkspaceAcl.name)
|
||||
.join(Users.name, Users.col.id, WorkspaceAcl.col.userId)
|
||||
.where(WorkspaceAcl.col.workspaceId, workspaceId)
|
||||
})
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
GetDefaultRegion,
|
||||
GetWorkspaceRoleToDefaultProjectRoleMapping,
|
||||
GetWorkspaceSeatTypeToProjectRoleMapping,
|
||||
IntersectProjectCollaboratorsAndWorkspaceCollaborators,
|
||||
QueryAllWorkspaceProjects,
|
||||
UpdateWorkspaceRole
|
||||
} from '@/modules/workspaces/domain/operations'
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
} from '@/modules/workspaces/errors/workspace'
|
||||
import {
|
||||
GetProject,
|
||||
GetProjectCollaborators,
|
||||
UpdateProject,
|
||||
UpsertProjectRole
|
||||
} from '@/modules/core/domain/projects/operations'
|
||||
@@ -22,6 +22,7 @@ import { Roles, StreamRoles } from '@speckle/shared'
|
||||
import { orderByWeight } from '@/modules/shared/domain/rolesAndScopes/logic'
|
||||
import coreUserRoles from '@/modules/core/roles'
|
||||
import {
|
||||
GetStreamCollaborators,
|
||||
GetUserStreamsPage,
|
||||
LegacyGetStreams
|
||||
} from '@/modules/core/domain/streams/operations'
|
||||
@@ -144,7 +145,7 @@ export const moveProjectToWorkspaceFactory =
|
||||
getProject: GetProject
|
||||
updateProject: UpdateProject
|
||||
upsertProjectRole: UpsertProjectRole
|
||||
getProjectCollaborators: GetProjectCollaborators
|
||||
getProjectCollaborators: GetStreamCollaborators
|
||||
getWorkspaceRolesAndSeats: GetWorkspaceRolesAndSeats
|
||||
getWorkspaceRoleToDefaultProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping
|
||||
updateWorkspaceRole: UpdateWorkspaceRole
|
||||
@@ -168,7 +169,7 @@ export const moveProjectToWorkspaceFactory =
|
||||
// Update roles for current project members
|
||||
const [workspace, projectTeam, workspaceTeam] = await Promise.all([
|
||||
getWorkspaceWithPlan({ workspaceId }),
|
||||
getProjectCollaborators({ projectId }),
|
||||
getProjectCollaborators(projectId),
|
||||
getWorkspaceRolesAndSeats({ workspaceId })
|
||||
])
|
||||
if (!workspace) throw new WorkspaceNotFoundError()
|
||||
@@ -357,3 +358,17 @@ export const createWorkspaceProjectFactory =
|
||||
|
||||
return project
|
||||
}
|
||||
|
||||
export const getMoveProjectToWorkspaceDryRunFactory =
|
||||
(deps: {
|
||||
intersectProjectCollaboratorsAndWorkspaceCollaborators: IntersectProjectCollaboratorsAndWorkspaceCollaborators
|
||||
}) =>
|
||||
async (args: { projectId: string; workspaceId: string }) => {
|
||||
const addedToWorkspace =
|
||||
await deps.intersectProjectCollaboratorsAndWorkspaceCollaborators({
|
||||
projectId: args.projectId,
|
||||
workspaceId: args.workspaceId
|
||||
})
|
||||
|
||||
return { addedToWorkspace }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { createRandomEmail } from '@/modules/core/helpers/testHelpers'
|
||||
import { intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory } from '@/modules/workspaces/repositories/projects'
|
||||
import {
|
||||
assignToWorkspaces,
|
||||
BasicTestWorkspace,
|
||||
createTestWorkspace
|
||||
} from '@/modules/workspaces/tests/helpers/creation'
|
||||
import { BasicTestUser, createTestUser, createTestUsers } from '@/test/authHelper'
|
||||
import {
|
||||
addAllToStream,
|
||||
BasicTestStream,
|
||||
createTestStream
|
||||
} from '@/test/speckle-helpers/streamHelper'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { db } from '@/db/knex'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory returns a function, that', () => {
|
||||
const adminUser: BasicTestUser = {
|
||||
id: '',
|
||||
email: createRandomEmail(),
|
||||
name: 'Mr. Workspace'
|
||||
}
|
||||
|
||||
const projectUsers: BasicTestUser[] = [
|
||||
{
|
||||
id: '',
|
||||
email: createRandomEmail(),
|
||||
name: 'John A. Speckle'
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
email: createRandomEmail(),
|
||||
name: 'John B. Speckle'
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
email: createRandomEmail(),
|
||||
name: 'John C. Speckle'
|
||||
}
|
||||
]
|
||||
|
||||
const workspaceUsers: BasicTestUser[] = [
|
||||
{
|
||||
id: '',
|
||||
email: createRandomEmail(),
|
||||
name: 'John X. Speckle'
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
email: createRandomEmail(),
|
||||
name: 'John Y. Speckle'
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
email: createRandomEmail(),
|
||||
name: 'John Z. Speckle'
|
||||
}
|
||||
]
|
||||
|
||||
const project: BasicTestStream = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
name: cryptoRandomString({ length: 9 }),
|
||||
isPublic: true
|
||||
}
|
||||
|
||||
const workspace: BasicTestWorkspace = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
name: cryptoRandomString({ length: 9 }),
|
||||
slug: ''
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
await createTestUser(adminUser)
|
||||
await createTestUsers([...projectUsers, ...workspaceUsers])
|
||||
|
||||
await createTestStream(project, adminUser)
|
||||
await addAllToStream(project, projectUsers)
|
||||
await createTestWorkspace(workspace, adminUser)
|
||||
await assignToWorkspaces(workspaceUsers.map((user) => [workspace, user, null]))
|
||||
})
|
||||
|
||||
it('returns users that are project members but not members of the target workspace', async () => {
|
||||
const result = await intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory({
|
||||
db
|
||||
})({
|
||||
projectId: project.id,
|
||||
workspaceId: workspace.id
|
||||
})
|
||||
|
||||
expect(result.length).to.equal(3)
|
||||
expect(
|
||||
result.every((resultUser) =>
|
||||
projectUsers.some((projectUser) => projectUser.id === resultUser.id)
|
||||
)
|
||||
).to.equal(true)
|
||||
})
|
||||
|
||||
it('does not return project users that are already members of the workspace', async () => {
|
||||
const result = await intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory({
|
||||
db
|
||||
})({
|
||||
projectId: project.id,
|
||||
workspaceId: workspace.id
|
||||
})
|
||||
|
||||
expect(
|
||||
result.some((resultUser) =>
|
||||
workspaceUsers.some((workspaceUser) => workspaceUser.id === resultUser.id)
|
||||
)
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('does not return workspace admin or project owner', async () => {
|
||||
const result = await intersectProjectCollaboratorsAndWorkspaceCollaboratorsFactory({
|
||||
db
|
||||
})({
|
||||
projectId: project.id,
|
||||
workspaceId: workspace.id
|
||||
})
|
||||
|
||||
expect(result.some((user) => user.id === adminUser.id)).to.equal(false)
|
||||
})
|
||||
})
|
||||
@@ -40,3 +40,5 @@ export type WorkspaceCollaboratorGraphQLReturn = WorkspaceTeamMember
|
||||
export type WorkspacePermissionChecksGraphQLReturn = {
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
export type ProjectMoveToWorkspaceDryRunGraphQLReturn = LimitedUserRecord[]
|
||||
|
||||
@@ -2012,6 +2012,8 @@ export type Project = {
|
||||
* real or fake (e.g., with a foo/bar model, it will be nested under foo even if such a model doesn't actually exist)
|
||||
*/
|
||||
modelsTree: ModelsTreeItemCollection;
|
||||
/** Returns information about the potential effects of moving a project to a given workspace. */
|
||||
moveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRun;
|
||||
name: Scalars['String']['output'];
|
||||
object?: Maybe<Object>;
|
||||
/** Pending project access requests */
|
||||
@@ -2110,6 +2112,11 @@ export type ProjectModelsTreeArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMoveToWorkspaceDryRunArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectObjectArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
@@ -2430,6 +2437,17 @@ export const ProjectModelsUpdatedMessageType = {
|
||||
} as const;
|
||||
|
||||
export type ProjectModelsUpdatedMessageType = typeof ProjectModelsUpdatedMessageType[keyof typeof ProjectModelsUpdatedMessageType];
|
||||
export type ProjectMoveToWorkspaceDryRun = {
|
||||
__typename?: 'ProjectMoveToWorkspaceDryRun';
|
||||
addedToWorkspace: Array<LimitedUser>;
|
||||
addedToWorkspaceTotalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMoveToWorkspaceDryRunAddedToWorkspaceArgs = {
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export type ProjectMutations = {
|
||||
__typename?: 'ProjectMutations';
|
||||
/** Access request related mutations */
|
||||
|
||||
Reference in New Issue
Block a user