From 9a18a6e1c20092c7cd17fc927abe9d7cd0bcf078 Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Fri, 25 Apr 2025 11:02:37 +0300 Subject: [PATCH] feat(server): ProjectCollaborator.workspaceRole (#4598) --- .../workspacesCore/typedefs/projects.graphql | 7 ++++++ .../modules/core/graph/generated/graphql.ts | 3 +++ .../graph/generated/graphql.ts | 2 ++ .../modules/workspaces/domain/operations.ts | 13 ++++++++++ .../graph/dataloaders/workspaces.ts | 25 +++++++++++++++++-- .../workspaces/graph/resolvers/workspaces.ts | 19 ++++++++++++-- .../workspaces/repositories/workspaces.ts | 21 ++++++++++++++++ .../graph/resolvers/workspacesCore.ts | 5 ++++ .../server/test/graphql/generated/graphql.ts | 2 ++ 9 files changed, 93 insertions(+), 4 deletions(-) diff --git a/packages/server/assets/workspacesCore/typedefs/projects.graphql b/packages/server/assets/workspacesCore/typedefs/projects.graphql index cfbd240f4..462d3e9e5 100644 --- a/packages/server/assets/workspacesCore/typedefs/projects.graphql +++ b/packages/server/assets/workspacesCore/typedefs/projects.graphql @@ -9,3 +9,10 @@ extend type Project { limit: Int! = 25 ): WorkspaceCollaboratorCollection! } + +extend type ProjectCollaborator { + """ + The collaborator's workspace role for the workspace this project is in, if any + """ + workspaceRole: String +} diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 826ce5f26..798695c37 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -2330,6 +2330,8 @@ export type ProjectCollaborator = { /** The collaborator's workspace seat type for the workspace this project is in */ seatType?: Maybe; user: LimitedUser; + /** The collaborator's workspace role for the workspace this project is in, if any */ + workspaceRole?: Maybe; }; export type ProjectCollection = { @@ -6719,6 +6721,7 @@ export type ProjectCollaboratorResolvers; seatType?: Resolver, ParentType, ContextType>; user?: Resolver; + workspaceRole?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; 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 5a2aac972..489d738fd 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -2310,6 +2310,8 @@ export type ProjectCollaborator = { /** The collaborator's workspace seat type for the workspace this project is in */ seatType?: Maybe; user: LimitedUser; + /** The collaborator's workspace role for the workspace this project is in, if any */ + workspaceRole?: Maybe; }; export type ProjectCollection = { diff --git a/packages/server/modules/workspaces/domain/operations.ts b/packages/server/modules/workspaces/domain/operations.ts index def1271eb..e20029883 100644 --- a/packages/server/modules/workspaces/domain/operations.ts +++ b/packages/server/modules/workspaces/domain/operations.ts @@ -180,6 +180,19 @@ export type GetWorkspaceRolesForUser = ( options?: GetWorkspaceRolesForUserOptions ) => Promise +export type GetWorkspacesRolesForUsers = ( + reqs: Array<{ + userId: string + workspaceId: string + }> +) => Promise<{ + [workspaceId: string]: + | { + [userId: string]: WorkspaceAcl | undefined + } + | undefined +}> + /** Repository-level change to workspace acl record */ export type UpsertWorkspaceRole = (args: WorkspaceAcl) => Promise diff --git a/packages/server/modules/workspaces/graph/dataloaders/workspaces.ts b/packages/server/modules/workspaces/graph/dataloaders/workspaces.ts index 2be3043b5..ca5d535e7 100644 --- a/packages/server/modules/workspaces/graph/dataloaders/workspaces.ts +++ b/packages/server/modules/workspaces/graph/dataloaders/workspaces.ts @@ -3,9 +3,11 @@ import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper import { getWorkspaceDomainsFactory, getWorkspacesFactory, - getWorkspacesProjectsCountsFactory + getWorkspacesProjectsCountsFactory, + getWorkspacesRolesForUsersFactory } from '@/modules/workspaces/repositories/workspaces' import { + WorkspaceAcl, WorkspaceDomain, WorkspaceWithOptionalRole } from '@/modules/workspacesCore/domain/types' @@ -23,6 +25,7 @@ const dataLoadersDefinition = defineRequestDataloaders( const getWorkspaces = getWorkspacesFactory({ db }) const getWorkspaceDomains = getWorkspaceDomainsFactory({ db }) const getWorkspacesProjectsCounts = getWorkspacesProjectsCountsFactory({ db }) + const getWorkspacesRolesForUsers = getWorkspacesRolesForUsersFactory({ db }) return { workspaces: { @@ -46,7 +49,25 @@ const dataLoadersDefinition = defineRequestDataloaders( workspaceIds: ids.slice() }) return ids.map((id) => results[id]) - }) + }), + /** + * Get workspace role + */ + getWorkspaceRole: createLoader< + { userId: string; workspaceId: string }, + WorkspaceAcl | null, + string + >( + async (idPairs) => { + const results = await getWorkspacesRolesForUsers(idPairs.slice()) + return idPairs.map(({ userId, workspaceId }) => { + return results[workspaceId]?.[userId] || null + }) + }, + { + cacheKeyFn: (args) => `${args.userId}-${args.workspaceId}` + } + ) }, workspaceDomains: { /** diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index fa644b1c6..cfba612ce 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -1501,8 +1501,11 @@ export = FF_WORKSPACES_MODULE_ENABLED return getWorkspaceCreationStateFactory({ db })({ workspaceId: parent.id }) }, role: async (parent, _args, ctx) => { - const workspace = await ctx.loaders.workspaces!.getWorkspace.load(parent.id) - return workspace?.role || null + const acl = await ctx.loaders.workspaces!.getWorkspaceRole.load({ + userId: ctx.userId!, + workspaceId: parent.id + }) + return acl?.role || null }, team: async (parent, args) => { const roles = args.filter?.roles?.map((r) => { @@ -1753,6 +1756,18 @@ export = FF_WORKSPACES_MODULE_ENABLED return parent.email } }, + ProjectCollaborator: { + workspaceRole: async (parent, _args, ctx) => { + const project = await ctx.loaders.streams.getStream.load(parent.projectId) + if (!project?.workspaceId) return null + + const acl = await ctx.loaders.workspaces!.getWorkspaceRole.load({ + userId: parent.user.id, + workspaceId: project.workspaceId + }) + return acl?.role || null + } + }, User: { discoverableWorkspaces: async (_parent, _args, context) => { if (!context.userId) { diff --git a/packages/server/modules/workspaces/repositories/workspaces.ts b/packages/server/modules/workspaces/repositories/workspaces.ts index 9295d4aa4..a18b8a596 100644 --- a/packages/server/modules/workspaces/repositories/workspaces.ts +++ b/packages/server/modules/workspaces/repositories/workspaces.ts @@ -31,6 +31,7 @@ import { GetWorkspaceWithDomains, GetWorkspaces, GetWorkspacesProjectsCounts, + GetWorkspacesRolesForUsers, QueryWorkspaces, StoreWorkspaceDomain, UpsertWorkspace, @@ -271,6 +272,26 @@ export const getWorkspaceRoleForUserFactory = ) } +export const getWorkspacesRolesForUsersFactory = + (deps: { db: Knex }): GetWorkspacesRolesForUsers => + async (reqs) => { + const query = tables.workspacesAcl(deps.db).whereIn( + [DbWorkspaceAcl.col.userId, DbWorkspaceAcl.col.workspaceId], + reqs.map(({ userId, workspaceId }) => [userId, workspaceId]) + ) + const results = await query + + return results.reduce((acc, acl) => { + const { userId, workspaceId } = acl + if (!acc[workspaceId]) { + acc[workspaceId] = {} + } + + acc[workspaceId][userId] = acl + return acc + }, {} as Awaited>) + } + export const getWorkspaceRolesForUserFactory = ({ db }: { db: Knex }): GetWorkspaceRolesForUser => async ({ userId }, options) => { diff --git a/packages/server/modules/workspacesCore/graph/resolvers/workspacesCore.ts b/packages/server/modules/workspacesCore/graph/resolvers/workspacesCore.ts index 6c1ae8550..8d3898831 100644 --- a/packages/server/modules/workspacesCore/graph/resolvers/workspacesCore.ts +++ b/packages/server/modules/workspacesCore/graph/resolvers/workspacesCore.ts @@ -103,6 +103,11 @@ export = !FF_WORKSPACES_MODULE_ENABLED throw new WorkspacesModuleDisabledError() } }, + ProjectCollaborator: { + workspaceRole: async () => { + throw new WorkspacesModuleDisabledError() + } + }, Project: { workspace: async () => { // Return type is always workspace or null, to make the FE implementation easier we force return null in this case diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index adacae94f..0a0d461e0 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -2311,6 +2311,8 @@ export type ProjectCollaborator = { /** The collaborator's workspace seat type for the workspace this project is in */ seatType?: Maybe; user: LimitedUser; + /** The collaborator's workspace role for the workspace this project is in, if any */ + workspaceRole?: Maybe; }; export type ProjectCollection = {