From fd68c6ef2a496a2744ae65d57b73eccd9304b5d6 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Tue, 25 Feb 2025 12:19:21 +0100 Subject: [PATCH] feat(workspaces): user workspace join requests (#4023) * feat(workspaces): user workspace join requests * chore(workspaces): return limited workspace * chore(workspaces): fix tests * chore(workspaces): add index for userId * chore(shared): fix totalcount on getpaginateditems * chore(workspaces): add workspace core resolvers to throw specific error --- .../lib/common/generated/gql/graphql.ts | 49 ++++ .../typedefs/workspaceJoinRequests.graphql | 29 ++ packages/server/codegen.yml | 1 + .../modules/core/graph/generated/graphql.ts | 59 +++- .../graph/generated/graphql.ts | 32 +++ .../modules/shared/services/paginatedItems.ts | 9 +- .../graph/resolvers/workspaceJoinRequests.ts | 260 +++++++++++------- .../repositories/workspaceJoinRequests.ts | 56 +++- .../workspaceJoinRequests.graph.spec.ts | 73 +++++ .../graph/resolvers/workspaceJoinRequests.ts | 47 ++++ .../workspacesCore/helpers/graphTypes.ts | 1 + ...6_index_user_id_workspace_join_requests.ts | 15 + .../server/test/graphql/generated/graphql.ts | 38 +++ packages/server/test/graphql/users.ts | 25 ++ 14 files changed, 581 insertions(+), 113 deletions(-) create mode 100644 packages/server/modules/workspacesCore/graph/resolvers/workspaceJoinRequests.ts create mode 100644 packages/server/modules/workspacesCore/migrations/20250219100906_index_user_id_workspace_join_requests.ts diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index 238ff3d07..9100e2e82 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -1153,6 +1153,22 @@ export type LimitedWorkspace = { slug: Scalars['String']['output']; }; +export type LimitedWorkspaceJoinRequest = { + __typename?: 'LimitedWorkspaceJoinRequest'; + createdAt: Scalars['DateTime']['output']; + id: Scalars['String']['output']; + status: WorkspaceJoinRequestStatus; + user: LimitedUser; + workspace: LimitedWorkspace; +}; + +export type LimitedWorkspaceJoinRequestCollection = { + __typename?: 'LimitedWorkspaceJoinRequestCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']['output']; +}; + export type MarkCommentViewedInput = { commentId: Scalars['String']['input']; projectId: Scalars['String']['input']; @@ -3707,6 +3723,7 @@ export type User = { versions: CountOnlyCollection; /** Get all invitations to workspaces that the active user has */ workspaceInvites: Array; + workspaceJoinRequests?: Maybe; /** Get the workspaces for the user */ workspaces: WorkspaceCollection; }; @@ -3808,6 +3825,17 @@ export type UserVersionsArgs = { }; +/** + * Full user type, should only be used in the context of admin operations or + * when a user is reading/writing info about himself + */ +export type UserWorkspaceJoinRequestsArgs = { + cursor?: InputMaybe; + filter?: InputMaybe; + limit?: Scalars['Int']['input']; +}; + + /** * Full user type, should only be used in the context of admin operations or * when a user is reading/writing info about himself @@ -4420,6 +4448,10 @@ export type WorkspaceJoinRequestCollection = { totalCount: Scalars['Int']['output']; }; +export type WorkspaceJoinRequestFilter = { + status?: InputMaybe; +}; + export type WorkspaceJoinRequestMutations = { __typename?: 'WorkspaceJoinRequestMutations'; approve: Scalars['Boolean']['output']; @@ -6993,6 +7025,8 @@ export type AllObjectTypes = { LegacyCommentViewerData: LegacyCommentViewerData, LimitedUser: LimitedUser, LimitedWorkspace: LimitedWorkspace, + LimitedWorkspaceJoinRequest: LimitedWorkspaceJoinRequest, + LimitedWorkspaceJoinRequestCollection: LimitedWorkspaceJoinRequestCollection, Model: Model, ModelCollection: ModelCollection, ModelMutations: ModelMutations, @@ -7474,6 +7508,18 @@ export type LimitedWorkspaceFieldArgs = { name: {}, slug: {}, } +export type LimitedWorkspaceJoinRequestFieldArgs = { + createdAt: {}, + id: {}, + status: {}, + user: {}, + workspace: {}, +} +export type LimitedWorkspaceJoinRequestCollectionFieldArgs = { + cursor: {}, + items: {}, + totalCount: {}, +} export type ModelFieldArgs = { author: {}, automationsStatus: {}, @@ -8078,6 +8124,7 @@ export type UserFieldArgs = { verified: {}, versions: UserVersionsArgs, workspaceInvites: {}, + workspaceJoinRequests: UserWorkspaceJoinRequestsArgs, workspaces: UserWorkspacesArgs, } export type UserAutomateInfoFieldArgs = { @@ -8401,6 +8448,8 @@ export type AllObjectFieldArgTypes = { LegacyCommentViewerData: LegacyCommentViewerDataFieldArgs, LimitedUser: LimitedUserFieldArgs, LimitedWorkspace: LimitedWorkspaceFieldArgs, + LimitedWorkspaceJoinRequest: LimitedWorkspaceJoinRequestFieldArgs, + LimitedWorkspaceJoinRequestCollection: LimitedWorkspaceJoinRequestCollectionFieldArgs, Model: ModelFieldArgs, ModelCollection: ModelCollectionFieldArgs, ModelMutations: ModelMutationsFieldArgs, diff --git a/packages/server/assets/workspacesCore/typedefs/workspaceJoinRequests.graphql b/packages/server/assets/workspacesCore/typedefs/workspaceJoinRequests.graphql index 370ab50a1..53e8393d2 100644 --- a/packages/server/assets/workspacesCore/typedefs/workspaceJoinRequests.graphql +++ b/packages/server/assets/workspacesCore/typedefs/workspaceJoinRequests.graphql @@ -23,3 +23,32 @@ type WorkspaceJoinRequestMutations { @hasScope(scope: "workspace:update") @hasWorkspaceRole(role: ADMIN) } + +type LimitedWorkspaceJoinRequest { + id: String! + workspace: LimitedWorkspace! + user: LimitedUser! + status: WorkspaceJoinRequestStatus! + createdAt: DateTime! +} + +type LimitedWorkspaceJoinRequestCollection { + totalCount: Int! + cursor: String + items: [LimitedWorkspaceJoinRequest!]! +} + +input WorkspaceJoinRequestFilter { + status: WorkspaceJoinRequestStatus +} + +extend type User { + workspaceJoinRequests( + filter: WorkspaceJoinRequestFilter + cursor: String + limit: Int! = 25 + ): LimitedWorkspaceJoinRequestCollection + @hasServerRole(role: SERVER_GUEST) + @hasScope(scope: "workspace:read") + @isOwner +} diff --git a/packages/server/codegen.yml b/packages/server/codegen.yml index fcd4c7933..da3e0a63b 100644 --- a/packages/server/codegen.yml +++ b/packages/server/codegen.yml @@ -66,6 +66,7 @@ generates: PendingWorkspaceCollaborator: '@/modules/workspacesCore/helpers/graphTypes#PendingWorkspaceCollaboratorGraphQLReturn' WorkspaceCollaborator: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceCollaboratorGraphQLReturn' WorkspaceJoinRequest: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceJoinRequestGraphQLReturn' + LimitedWorkspaceJoinRequest: '@/modules/workspacesCore/helpers/graphTypes#LimitedWorkspaceJoinRequestGraphQLReturn' Webhook: '@/modules/webhooks/helpers/graphTypes#WebhookGraphQLReturn' SmartTextEditorValue: '@/modules/core/services/richTextEditorService#SmartTextEditorValueGraphQLReturn' BlobMetadata: '@/modules/blobstorage/domain/types#BlobStorageItem' diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 3874bb632..a3aca3949 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -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, ProjectRoleGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes'; +import { WorkspaceGraphQLReturn, WorkspaceSsoGraphQLReturn, WorkspaceMutationsGraphQLReturn, WorkspaceJoinRequestMutationsGraphQLReturn, WorkspaceInviteMutationsGraphQLReturn, WorkspaceProjectMutationsGraphQLReturn, PendingWorkspaceCollaboratorGraphQLReturn, WorkspaceCollaboratorGraphQLReturn, WorkspaceJoinRequestGraphQLReturn, LimitedWorkspaceJoinRequestGraphQLReturn, ProjectRoleGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes'; import { WorkspaceBillingMutationsGraphQLReturn } from '@/modules/gatekeeper/helpers/graphTypes'; import { WebhookGraphQLReturn } from '@/modules/webhooks/helpers/graphTypes'; import { SmartTextEditorValueGraphQLReturn } from '@/modules/core/services/richTextEditorService'; @@ -1175,6 +1175,22 @@ export type LimitedWorkspace = { slug: Scalars['String']['output']; }; +export type LimitedWorkspaceJoinRequest = { + __typename?: 'LimitedWorkspaceJoinRequest'; + createdAt: Scalars['DateTime']['output']; + id: Scalars['String']['output']; + status: WorkspaceJoinRequestStatus; + user: LimitedUser; + workspace: LimitedWorkspace; +}; + +export type LimitedWorkspaceJoinRequestCollection = { + __typename?: 'LimitedWorkspaceJoinRequestCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']['output']; +}; + export type MarkCommentViewedInput = { commentId: Scalars['String']['input']; projectId: Scalars['String']['input']; @@ -3729,6 +3745,7 @@ export type User = { versions: CountOnlyCollection; /** Get all invitations to workspaces that the active user has */ workspaceInvites: Array; + workspaceJoinRequests?: Maybe; /** Get the workspaces for the user */ workspaces: WorkspaceCollection; }; @@ -3830,6 +3847,17 @@ export type UserVersionsArgs = { }; +/** + * Full user type, should only be used in the context of admin operations or + * when a user is reading/writing info about himself + */ +export type UserWorkspaceJoinRequestsArgs = { + cursor?: InputMaybe; + filter?: InputMaybe; + limit?: Scalars['Int']['input']; +}; + + /** * Full user type, should only be used in the context of admin operations or * when a user is reading/writing info about himself @@ -4442,6 +4470,10 @@ export type WorkspaceJoinRequestCollection = { totalCount: Scalars['Int']['output']; }; +export type WorkspaceJoinRequestFilter = { + status?: InputMaybe; +}; + export type WorkspaceJoinRequestMutations = { __typename?: 'WorkspaceJoinRequestMutations'; approve: Scalars['Boolean']['output']; @@ -4929,6 +4961,8 @@ export type ResolversTypes = { LegacyCommentViewerData: ResolverTypeWrapper; LimitedUser: ResolverTypeWrapper; LimitedWorkspace: ResolverTypeWrapper; + LimitedWorkspaceJoinRequest: ResolverTypeWrapper; + LimitedWorkspaceJoinRequestCollection: ResolverTypeWrapper & { items: Array }>; MarkCommentViewedInput: MarkCommentViewedInput; MarkReceivedVersionInput: MarkReceivedVersionInput; Model: ResolverTypeWrapper; @@ -5097,6 +5131,7 @@ export type ResolversTypes = { WorkspaceInviteUseInput: WorkspaceInviteUseInput; WorkspaceJoinRequest: ResolverTypeWrapper; WorkspaceJoinRequestCollection: ResolverTypeWrapper & { items: Array }>; + WorkspaceJoinRequestFilter: WorkspaceJoinRequestFilter; WorkspaceJoinRequestMutations: ResolverTypeWrapper; WorkspaceJoinRequestStatus: WorkspaceJoinRequestStatus; WorkspaceMutations: ResolverTypeWrapper; @@ -5231,6 +5266,8 @@ export type ResolversParentTypes = { LegacyCommentViewerData: LegacyCommentViewerData; LimitedUser: LimitedUserGraphQLReturn; LimitedWorkspace: LimitedWorkspace; + LimitedWorkspaceJoinRequest: LimitedWorkspaceJoinRequestGraphQLReturn; + LimitedWorkspaceJoinRequestCollection: Omit & { items: Array }; MarkCommentViewedInput: MarkCommentViewedInput; MarkReceivedVersionInput: MarkReceivedVersionInput; Model: ModelGraphQLReturn; @@ -5379,6 +5416,7 @@ export type ResolversParentTypes = { WorkspaceInviteUseInput: WorkspaceInviteUseInput; WorkspaceJoinRequest: WorkspaceJoinRequestGraphQLReturn; WorkspaceJoinRequestCollection: Omit & { items: Array }; + WorkspaceJoinRequestFilter: WorkspaceJoinRequestFilter; WorkspaceJoinRequestMutations: WorkspaceJoinRequestMutationsGraphQLReturn; WorkspaceMutations: WorkspaceMutationsGraphQLReturn; WorkspacePlan: WorkspacePlan; @@ -5928,6 +5966,22 @@ export type LimitedWorkspaceResolvers; }; +export type LimitedWorkspaceJoinRequestResolvers = { + createdAt?: Resolver; + id?: Resolver; + status?: Resolver; + user?: Resolver; + workspace?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type LimitedWorkspaceJoinRequestCollectionResolvers = { + cursor?: Resolver, ParentType, ContextType>; + items?: Resolver, ParentType, ContextType>; + totalCount?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type ModelResolvers = { author?: Resolver, ParentType, ContextType>; automationsStatus?: Resolver, ParentType, ContextType>; @@ -6653,6 +6707,7 @@ export type UserResolvers, ParentType, ContextType>; versions?: Resolver>; workspaceInvites?: Resolver, ParentType, ContextType>; + workspaceJoinRequests?: Resolver, ParentType, ContextType, RequireFields>; workspaces?: Resolver>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -7065,6 +7120,8 @@ export type Resolvers = { LegacyCommentViewerData?: LegacyCommentViewerDataResolvers; LimitedUser?: LimitedUserResolvers; LimitedWorkspace?: LimitedWorkspaceResolvers; + LimitedWorkspaceJoinRequest?: LimitedWorkspaceJoinRequestResolvers; + LimitedWorkspaceJoinRequestCollection?: LimitedWorkspaceJoinRequestCollectionResolvers; Model?: ModelResolvers; ModelCollection?: ModelCollectionResolvers; ModelMutations?: ModelMutationsResolvers; diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index 347db92f8..96f54d600 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -1156,6 +1156,22 @@ export type LimitedWorkspace = { slug: Scalars['String']['output']; }; +export type LimitedWorkspaceJoinRequest = { + __typename?: 'LimitedWorkspaceJoinRequest'; + createdAt: Scalars['DateTime']['output']; + id: Scalars['String']['output']; + status: WorkspaceJoinRequestStatus; + user: LimitedUser; + workspace: LimitedWorkspace; +}; + +export type LimitedWorkspaceJoinRequestCollection = { + __typename?: 'LimitedWorkspaceJoinRequestCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']['output']; +}; + export type MarkCommentViewedInput = { commentId: Scalars['String']['input']; projectId: Scalars['String']['input']; @@ -3710,6 +3726,7 @@ export type User = { versions: CountOnlyCollection; /** Get all invitations to workspaces that the active user has */ workspaceInvites: Array; + workspaceJoinRequests?: Maybe; /** Get the workspaces for the user */ workspaces: WorkspaceCollection; }; @@ -3811,6 +3828,17 @@ export type UserVersionsArgs = { }; +/** + * Full user type, should only be used in the context of admin operations or + * when a user is reading/writing info about himself + */ +export type UserWorkspaceJoinRequestsArgs = { + cursor?: InputMaybe; + filter?: InputMaybe; + limit?: Scalars['Int']['input']; +}; + + /** * Full user type, should only be used in the context of admin operations or * when a user is reading/writing info about himself @@ -4423,6 +4451,10 @@ export type WorkspaceJoinRequestCollection = { totalCount: Scalars['Int']['output']; }; +export type WorkspaceJoinRequestFilter = { + status?: InputMaybe; +}; + export type WorkspaceJoinRequestMutations = { __typename?: 'WorkspaceJoinRequestMutations'; approve: Scalars['Boolean']['output']; diff --git a/packages/server/modules/shared/services/paginatedItems.ts b/packages/server/modules/shared/services/paginatedItems.ts index 4960b46d5..99f9e3d6f 100644 --- a/packages/server/modules/shared/services/paginatedItems.ts +++ b/packages/server/modules/shared/services/paginatedItems.ts @@ -23,12 +23,19 @@ export const getPaginatedItemsFactory = getTotalCount: (args: Omit) => Promise }) => async (args: TArgs): Promise> => { + const totalCount = await getTotalCount(args) + if (args.limit === 0) { + return { + cursor: null, + items: [], + totalCount + } + } const maybeDecodedCursor = args.cursor ? decodeIsoDateCursor(args.cursor) : null const items = await getItems({ ...args, cursor: maybeDecodedCursor ?? undefined }) - const totalCount = await getTotalCount(args) let cursor = null if (items.length === args.limit) { diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaceJoinRequests.ts b/packages/server/modules/workspaces/graph/resolvers/workspaceJoinRequests.ts index 45888a9a4..9d0406dc9 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaceJoinRequests.ts @@ -6,6 +6,7 @@ import { getUserFactory } from '@/modules/core/repositories/users' import { renderEmail } from '@/modules/emails/services/emailRendering' import { sendEmail } from '@/modules/emails/services/sending' import { commandFactory } from '@/modules/shared/command' +import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { getEventBus } from '@/modules/shared/services/eventBus' import { getPaginatedItemsFactory } from '@/modules/shared/services/paginatedItems' import { @@ -14,8 +15,10 @@ import { } from '@/modules/workspaces/domain/operations' import { countAdminWorkspaceJoinRequestsFactory, + countWorkspaceJoinRequestsFactory, getAdminWorkspaceJoinRequestsFactory, getWorkspaceJoinRequestFactory, + getWorkspaceJoinRequestsFactory, updateWorkspaceJoinRequestStatusFactory } from '@/modules/workspaces/repositories/workspaceJoinRequests' import { @@ -33,117 +36,160 @@ import { WorkspaceJoinRequestGraphQLReturn } from '@/modules/workspacesCore/help const eventBus = getEventBus() -export default { - Workspace: { - adminWorkspacesJoinRequests: async (parent, args, ctx) => { - const { filter, cursor, limit } = args +const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() - return await getPaginatedItemsFactory< - { - limit: number - cursor?: string - filter: { - workspaceId: string - userId: string - status?: WorkspaceJoinRequestStatus | null - } - }, - WorkspaceJoinRequestGraphQLReturn - >({ - getItems: getAdminWorkspaceJoinRequestsFactory({ db }), - getTotalCount: countAdminWorkspaceJoinRequestsFactory({ db }) - })({ - filter: { - workspaceId: parent.id, - status: filter?.status ?? undefined, - userId: ctx.userId! // This is the worskpace admin, not the request userId - }, - cursor: cursor ?? undefined, - limit - }) - } - }, - WorkspaceJoinRequest: { - id: async (parent) => { - return parent.userId + parent.workspaceId - }, - user: async (parent, _args, ctx) => { - return await ctx.loaders.users.getUser.load(parent.userId) - }, - workspace: async (parent, _args, ctx) => { - return await ctx.loaders.workspaces!.getWorkspace.load(parent.workspaceId) - } - }, - Mutation: { - workspaceJoinRequestMutations: () => ({}) - }, - WorkspaceJoinRequestMutations: { - approve: async (_parent, args) => { - const approveWorkspaceJoinRequest = commandFactory({ - db, - eventBus, - operationFactory: ({ db, emit }) => { - const updateWorkspaceJoinRequestStatus = - updateWorkspaceJoinRequestStatusFactory({ - db - }) - const sendWorkspaceJoinRequestApprovedEmail = - sendWorkspaceJoinRequestApprovedEmailFactory({ - renderEmail, - sendEmail, - getServerInfo: getServerInfoFactory({ db }), - getUserEmails: findEmailsByUserIdFactory({ db }) - }) - return approveWorkspaceJoinRequestFactory({ - updateWorkspaceJoinRequestStatus, - sendWorkspaceJoinRequestApprovedEmail, - getUserById: getUserFactory({ db }), - getWorkspace: getWorkspaceFactory({ db }), - getWorkspaceJoinRequest: getWorkspaceJoinRequestFactory({ - db - }), - upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }), - emit +export default FF_WORKSPACES_MODULE_ENABLED + ? ({ + Workspace: { + adminWorkspacesJoinRequests: async (parent, args, ctx) => { + const { filter, cursor, limit } = args + + return await getPaginatedItemsFactory< + { + limit: number + cursor?: string + filter: { + workspaceId: string + userId: string + status?: WorkspaceJoinRequestStatus | null + } + }, + WorkspaceJoinRequestGraphQLReturn + >({ + getItems: getAdminWorkspaceJoinRequestsFactory({ db }), + getTotalCount: countAdminWorkspaceJoinRequestsFactory({ db }) + })({ + filter: { + workspaceId: parent.id, + status: filter?.status ?? undefined, + userId: ctx.userId! // This is the worskpace admin, not the request userId + }, + cursor: cursor ?? undefined, + limit }) } - }) - return await approveWorkspaceJoinRequest({ - userId: args.input.userId, - workspaceId: args.input.workspaceId - }) - }, - deny: async (_parent, args) => { - const denyWorkspaceJoinRequest = commandFactory({ - db, - operationFactory: ({ db }) => { - const updateWorkspaceJoinRequestStatus = - updateWorkspaceJoinRequestStatusFactory({ - db - }) - const sendWorkspaceJoinRequestDeniedEmail = - sendWorkspaceJoinRequestDeniedEmailFactory({ - renderEmail, - sendEmail, - getServerInfo: getServerInfoFactory({ db }), - getUserEmails: findEmailsByUserIdFactory({ db }) - }) + }, + WorkspaceJoinRequest: { + id: async (parent) => { + return parent.userId + parent.workspaceId + }, + user: async (parent, _args, ctx) => { + return await ctx.loaders.users.getUser.load(parent.userId) + }, + workspace: async (parent, _args, ctx) => { + return await ctx.loaders.workspaces!.getWorkspace.load(parent.workspaceId) + } + }, + LimitedWorkspaceJoinRequest: { + id: async (parent) => { + return parent.userId + parent.workspaceId + }, + user: async (parent, _args, ctx) => { + return await ctx.loaders.users.getUser.load(parent.userId) + }, + workspace: async (parent, _args, ctx) => { + return await ctx.loaders.workspaces!.getWorkspace.load(parent.workspaceId) + } + }, + User: { + workspaceJoinRequests: async (parent, args) => { + const { filter, cursor, limit } = args - return denyWorkspaceJoinRequestFactory({ - updateWorkspaceJoinRequestStatus, - sendWorkspaceJoinRequestDeniedEmail, - getUserById: getUserFactory({ db }), - getWorkspace: getWorkspaceFactory({ db }), - getWorkspaceJoinRequest: getWorkspaceJoinRequestFactory({ - db - }) + return await getPaginatedItemsFactory< + { + limit: number + cursor?: string + filter: { + userId: string + status?: WorkspaceJoinRequestStatus | null + } + }, + WorkspaceJoinRequestGraphQLReturn + >({ + getItems: getWorkspaceJoinRequestsFactory({ db }), + getTotalCount: countWorkspaceJoinRequestsFactory({ db }) + })({ + filter: { + userId: parent.id, + status: filter?.status ?? undefined + }, + cursor: cursor ?? undefined, + limit }) } - }) + }, + Mutation: { + workspaceJoinRequestMutations: () => ({}) + }, + WorkspaceJoinRequestMutations: { + approve: async (_parent, args) => { + const approveWorkspaceJoinRequest = + commandFactory({ + db, + eventBus, + operationFactory: ({ db, emit }) => { + const updateWorkspaceJoinRequestStatus = + updateWorkspaceJoinRequestStatusFactory({ + db + }) + const sendWorkspaceJoinRequestApprovedEmail = + sendWorkspaceJoinRequestApprovedEmailFactory({ + renderEmail, + sendEmail, + getServerInfo: getServerInfoFactory({ db }), + getUserEmails: findEmailsByUserIdFactory({ db }) + }) + return approveWorkspaceJoinRequestFactory({ + updateWorkspaceJoinRequestStatus, + sendWorkspaceJoinRequestApprovedEmail, + getUserById: getUserFactory({ db }), + getWorkspace: getWorkspaceFactory({ db }), + getWorkspaceJoinRequest: getWorkspaceJoinRequestFactory({ + db + }), + upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }), + emit + }) + } + }) + return await approveWorkspaceJoinRequest({ + userId: args.input.userId, + workspaceId: args.input.workspaceId + }) + }, + deny: async (_parent, args) => { + const denyWorkspaceJoinRequest = commandFactory({ + db, + operationFactory: ({ db }) => { + const updateWorkspaceJoinRequestStatus = + updateWorkspaceJoinRequestStatusFactory({ + db + }) + const sendWorkspaceJoinRequestDeniedEmail = + sendWorkspaceJoinRequestDeniedEmailFactory({ + renderEmail, + sendEmail, + getServerInfo: getServerInfoFactory({ db }), + getUserEmails: findEmailsByUserIdFactory({ db }) + }) - return await denyWorkspaceJoinRequest({ - userId: args.input.userId, - workspaceId: args.input.workspaceId - }) - } - } -} as Resolvers + return denyWorkspaceJoinRequestFactory({ + updateWorkspaceJoinRequestStatus, + sendWorkspaceJoinRequestDeniedEmail, + getUserById: getUserFactory({ db }), + getWorkspace: getWorkspaceFactory({ db }), + getWorkspaceJoinRequest: getWorkspaceJoinRequestFactory({ + db + }) + }) + } + }) + + return await denyWorkspaceJoinRequest({ + userId: args.input.userId, + workspaceId: args.input.workspaceId + }) + } + } + } as Resolvers) + : {} diff --git a/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts b/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts index b85b331d9..49faaa428 100644 --- a/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts @@ -13,6 +13,7 @@ import { } from '@/modules/workspacesCore/helpers/db' import { Roles } from '@speckle/shared' import { Knex } from 'knex' +import { SetRequired } from 'type-fest' const tables = { workspaceJoinRequests: (db: Knex) => @@ -51,13 +52,13 @@ export const getWorkspaceJoinRequestFactory = } type WorkspaceJoinRequestFilter = { - workspaceId: string + workspaceId?: string status?: WorkspaceJoinRequestStatus | null userId: string } const adminWorkspaceJoinRequestsBaseQueryFactory = - (db: Knex) => (filter: WorkspaceJoinRequestFilter) => { + (db: Knex) => (filter: SetRequired) => { const query = tables .workspaceJoinRequests(db) .innerJoin( @@ -79,7 +80,7 @@ export const getAdminWorkspaceJoinRequestsFactory = cursor, limit }: { - filter: WorkspaceJoinRequestFilter + filter: SetRequired cursor?: string limit: number }) => { @@ -96,9 +97,56 @@ export const getAdminWorkspaceJoinRequestsFactory = export const countAdminWorkspaceJoinRequestsFactory = ({ db }: { db: Knex }) => - async ({ filter }: { filter: WorkspaceJoinRequestFilter }) => { + async ({ + filter + }: { + filter: SetRequired + }) => { const query = adminWorkspaceJoinRequestsBaseQueryFactory(db)(filter) const [res] = await query.count() return parseInt(res.count.toString()) } + +const workspaceJoinRequestsBaseQueryFactory = + (db: Knex) => (filter: WorkspaceJoinRequestFilter) => { + const query = tables + .workspaceJoinRequests(db) + .where(WorkspaceJoinRequests.col.userId, filter.userId) + if (filter.status) query.andWhere(WorkspaceJoinRequests.col.status, filter.status) + if (filter.userId) query.andWhere(WorkspaceJoinRequests.col.userId, filter.userId) + if (filter.workspaceId) + query.andWhere(WorkspaceJoinRequests.col.workspaceId, filter.workspaceId) + return query + } + +export const getWorkspaceJoinRequestsFactory = + ({ db }: { db: Knex }) => + ({ + filter, + cursor, + limit + }: { + filter: WorkspaceJoinRequestFilter + cursor?: string + limit: number + }) => { + const query = workspaceJoinRequestsBaseQueryFactory(db)(filter) + + if (cursor) { + query.andWhere(WorkspaceJoinRequests.col.createdAt, '<', cursor) + } + return query + .select(WorkspaceJoinRequests.cols) + .orderBy(WorkspaceJoinRequests.col.createdAt, 'desc') + .limit(limit) + } + +export const countWorkspaceJoinRequestsFactory = + ({ db }: { db: Knex }) => + async ({ filter }: { filter: WorkspaceJoinRequestFilter }) => { + const query = workspaceJoinRequestsBaseQueryFactory(db)(filter) + + const [res] = await query.count() + return parseInt(res.count.toString()) + } diff --git a/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.graph.spec.ts index 2fcaba4de..6a1b498f7 100644 --- a/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.graph.spec.ts @@ -7,6 +7,7 @@ import { createTestUser } from '@/test/authHelper' import { + GetActiveUserWithWorkspaceJoinRequestsDocument, GetWorkspaceWithJoinRequestsDocument, RequestToJoinWorkspaceDocument } from '@/test/graphql/generated/graphql' @@ -154,4 +155,76 @@ describe('WorkspaceJoinRequests GQL', () => { expect(items2[0].user.id).to.equal(user2.id) }) }) + + describe('User.workspaceJoinRequests', () => { + it('should return the workspace join requests for the user', async () => { + const admin = await createTestUser({ + name: 'admin user', + role: Roles.Server.User, + email: `${createRandomString()}@example.org`, + verified: true + }) + + const user = await createTestUser({ + name: 'user 1', + role: Roles.Server.User, + email: `${createRandomString()}@example.org`, + verified: true + }) + + const workspace1 = { + id: createRandomString(), + name: 'Workspace 1', + ownerId: admin.id, + description: '', + discoverabilityEnabled: true + } + await createTestWorkspace(workspace1, admin, { domain: 'example.org' }) + + const workspace2 = { + id: createRandomString(), + name: 'Workspace 2', + ownerId: admin.id, + description: '', + discoverabilityEnabled: true + } + await createTestWorkspace(workspace2, admin, { domain: 'example.org' }) + + const sessionUser = await login(user) + + // User requests to join workspace1 + const joinReq1 = await sessionUser.execute(RequestToJoinWorkspaceDocument, { + input: { + workspaceId: workspace1.id + } + }) + expect(joinReq1).to.not.haveGraphQLErrors() + + // User requests to join workspace2 + const joinReq2 = await sessionUser.execute(RequestToJoinWorkspaceDocument, { + input: { + workspaceId: workspace2.id + } + }) + expect(joinReq2).to.not.haveGraphQLErrors() + + const res = await sessionUser.execute( + GetActiveUserWithWorkspaceJoinRequestsDocument, + {} + ) + expect(res).to.not.haveGraphQLErrors() + + const { items, totalCount } = res.data!.activeUser!.workspaceJoinRequests! + + expect(totalCount).to.equal(2) + + expect(items).to.have.length(2) + expect(items[0].status).to.equal('pending') + expect(items[0].workspace.id).to.equal(workspace2.id) + expect(items[0].user.id).to.equal(user.id) + expect(items[1].status).to.equal('pending') + expect(items[1].workspace.id).to.equal(workspace1.id) + expect(items[1].user.id).to.equal(user.id) + }) + }) }) diff --git a/packages/server/modules/workspacesCore/graph/resolvers/workspaceJoinRequests.ts b/packages/server/modules/workspacesCore/graph/resolvers/workspaceJoinRequests.ts new file mode 100644 index 000000000..9c234dc5d --- /dev/null +++ b/packages/server/modules/workspacesCore/graph/resolvers/workspaceJoinRequests.ts @@ -0,0 +1,47 @@ +import { WorkspacesModuleDisabledError } from '@/modules/core/errors/workspaces' +import { Resolvers } from '@/modules/core/graph/generated/graphql' +import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' + +const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() + +export default !FF_WORKSPACES_MODULE_ENABLED + ? ({ + Workspace: { + adminWorkspacesJoinRequests: async () => { + throw new WorkspacesModuleDisabledError() + } + }, + WorkspaceJoinRequest: { + id: async () => { + throw new WorkspacesModuleDisabledError() + }, + user: async () => { + throw new WorkspacesModuleDisabledError() + }, + workspace: async () => { + throw new WorkspacesModuleDisabledError() + } + }, + LimitedWorkspaceJoinRequest: { + id: async () => { + throw new WorkspacesModuleDisabledError() + }, + user: async () => { + throw new WorkspacesModuleDisabledError() + }, + workspace: async () => { + throw new WorkspacesModuleDisabledError() + } + }, + User: { + workspaceJoinRequests: async () => { + throw new WorkspacesModuleDisabledError() + } + }, + Mutation: { + workspaceJoinRequestMutations: () => { + throw new WorkspacesModuleDisabledError() + } + } + } as Resolvers) + : {} diff --git a/packages/server/modules/workspacesCore/helpers/graphTypes.ts b/packages/server/modules/workspacesCore/helpers/graphTypes.ts index 548ae4a51..2b5347b69 100644 --- a/packages/server/modules/workspacesCore/helpers/graphTypes.ts +++ b/packages/server/modules/workspacesCore/helpers/graphTypes.ts @@ -7,6 +7,7 @@ import { WorkspaceRoles } from '@speckle/shared' export type WorkspaceGraphQLReturn = Workspace export type WorkspaceJoinRequestGraphQLReturn = WorkspaceJoinRequest +export type LimitedWorkspaceJoinRequestGraphQLReturn = WorkspaceJoinRequest export type WorkspaceBillingGraphQLReturn = { parent: Workspace } export type WorkspaceSsoGraphQLReturn = WorkspaceSsoProviderRecord export type WorkspaceMutationsGraphQLReturn = MutationsObjectGraphQLReturn diff --git a/packages/server/modules/workspacesCore/migrations/20250219100906_index_user_id_workspace_join_requests.ts b/packages/server/modules/workspacesCore/migrations/20250219100906_index_user_id_workspace_join_requests.ts new file mode 100644 index 000000000..c89649386 --- /dev/null +++ b/packages/server/modules/workspacesCore/migrations/20250219100906_index_user_id_workspace_join_requests.ts @@ -0,0 +1,15 @@ +import { Knex } from 'knex' + +const WORKSPACE_JOIN_REQUESTS_TABLE = 'workspace_join_requests' + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(WORKSPACE_JOIN_REQUESTS_TABLE, (table) => { + table.index('userId') + }) +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(WORKSPACE_JOIN_REQUESTS_TABLE, (table) => { + table.dropIndex('userId') + }) +} diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index a68025ac6..8b2df4e9c 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -1157,6 +1157,22 @@ export type LimitedWorkspace = { slug: Scalars['String']['output']; }; +export type LimitedWorkspaceJoinRequest = { + __typename?: 'LimitedWorkspaceJoinRequest'; + createdAt: Scalars['DateTime']['output']; + id: Scalars['String']['output']; + status: WorkspaceJoinRequestStatus; + user: LimitedUser; + workspace: LimitedWorkspace; +}; + +export type LimitedWorkspaceJoinRequestCollection = { + __typename?: 'LimitedWorkspaceJoinRequestCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']['output']; +}; + export type MarkCommentViewedInput = { commentId: Scalars['String']['input']; projectId: Scalars['String']['input']; @@ -3711,6 +3727,7 @@ export type User = { versions: CountOnlyCollection; /** Get all invitations to workspaces that the active user has */ workspaceInvites: Array; + workspaceJoinRequests?: Maybe; /** Get the workspaces for the user */ workspaces: WorkspaceCollection; }; @@ -3812,6 +3829,17 @@ export type UserVersionsArgs = { }; +/** + * Full user type, should only be used in the context of admin operations or + * when a user is reading/writing info about himself + */ +export type UserWorkspaceJoinRequestsArgs = { + cursor?: InputMaybe; + filter?: InputMaybe; + limit?: Scalars['Int']['input']; +}; + + /** * Full user type, should only be used in the context of admin operations or * when a user is reading/writing info about himself @@ -4424,6 +4452,10 @@ export type WorkspaceJoinRequestCollection = { totalCount: Scalars['Int']['output']; }; +export type WorkspaceJoinRequestFilter = { + status?: InputMaybe; +}; + export type WorkspaceJoinRequestMutations = { __typename?: 'WorkspaceJoinRequestMutations'; approve: Scalars['Boolean']['output']; @@ -5526,6 +5558,11 @@ export type GetActiveUserQueryVariables = Exact<{ [key: string]: never; }>; export type GetActiveUserQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', id: string, email?: string | null, name: string, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, role?: string | null } | null }; +export type GetActiveUserWithWorkspaceJoinRequestsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetActiveUserWithWorkspaceJoinRequestsQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', id: string, email?: string | null, name: string, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, role?: string | null, workspaceJoinRequests?: { __typename?: 'LimitedWorkspaceJoinRequestCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'LimitedWorkspaceJoinRequest', status: WorkspaceJoinRequestStatus, workspace: { __typename?: 'LimitedWorkspace', id: string, name: string }, user: { __typename?: 'LimitedUser', id: string, name: string } }> } | null } | null }; + export type GetOtherUserQueryVariables = Exact<{ id: Scalars['String']['input']; }>; @@ -5811,6 +5848,7 @@ export const CreateUserEmailDocument = {"kind":"Document","definitions":[{"kind" export const DeleteUserEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteUserEmail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteUserEmailInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUserMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"emailMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"delete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserWithEmails"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserWithEmails"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"emails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"primary"}}]}}]}}]} as unknown as DocumentNode; export const SetPrimaryUserEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetPrimaryUserEmail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SetPrimaryUserEmailInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUserMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"emailMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setPrimary"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserWithEmails"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserWithEmails"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"emails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"primary"}}]}}]}}]} as unknown as DocumentNode; export const GetActiveUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetActiveUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BaseUserFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BaseUserFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode; +export const GetActiveUserWithWorkspaceJoinRequestsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetActiveUserWithWorkspaceJoinRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BaseUserFields"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceJoinRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BaseUserFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode; export const GetOtherUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOtherUser"},"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":"otherUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BaseLimitedUserFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BaseLimitedUserFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}}]}}]} as unknown as DocumentNode; export const GetAdminUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminUsers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"25"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"0"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"query"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"NullValue"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminUsers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}},{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"registeredUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"invitedUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const GetPendingEmailVerificationStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPendingEmailVerificationStatus"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasPendingVerification"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/server/test/graphql/users.ts b/packages/server/test/graphql/users.ts index 1421600fa..87d2ab25b 100644 --- a/packages/server/test/graphql/users.ts +++ b/packages/server/test/graphql/users.ts @@ -47,6 +47,31 @@ const getActiveUserQuery = gql` ${baseUserFieldsFragment} ` +export const getActiveUserWithWorkspaceJoinRequestsQuery = gql` + query GetActiveUserWithWorkspaceJoinRequests { + activeUser { + ...BaseUserFields + workspaceJoinRequests { + totalCount + cursor + items { + workspace { + id + name + } + user { + id + name + } + status + } + } + } + } + + ${baseUserFieldsFragment} +` + const getOtherUserQuery = gql` query GetOtherUser($id: String!) { otherUser(id: $id) {