feat: Workspace/ProjectCollaborator/WorkspaceCollaborator seatType (#4284)

* Workspace & ProjectCollaborator seat type

* minor adjustment to FE

* minor adjustment to FE
This commit is contained in:
Kristaps Fabians Geikins
2025-03-31 13:07:35 +03:00
committed by GitHub
parent 8d1c45e6f8
commit a83bae8d84
19 changed files with 271 additions and 96 deletions
@@ -77,16 +77,12 @@
</template>
<script setup lang="ts">
import type {
SettingsWorkspacesMembersNewGuestsTable_WorkspaceFragment,
WorkspaceCollaborator
import {
WorkspaceSeatType,
type SettingsWorkspacesMembersNewGuestsTable_WorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import { graphql } from '~/lib/common/generated/gql'
import {
Roles,
type MaybeNullOrUndefined,
type WorkspaceSeatType
} from '@speckle/shared'
import { Roles, type MaybeNullOrUndefined } from '@speckle/shared'
import { settingsWorkspacesMembersSearchQuery } from '~~/lib/settings/graphql/queries'
import { useQuery } from '@vue/apollo-composable'
import { LearnMoreRolesSeatsUrl } from '~~/lib/common/helpers/route'
@@ -96,10 +92,12 @@ graphql(`
id
role
seatType
joinDate
user {
id
avatar
name
workspaceDomainPolicyCompliant
}
projectRoles {
role
@@ -154,9 +152,8 @@ const guests = computed(() => {
: props.workspace?.team.items
return (guestArray || [])
.filter(
(item): item is WorkspaceCollaborator => item.role === Roles.Workspace.Guest
)
.map((g) => ({ ...g, seatType: g.seatType || WorkspaceSeatType.Viewer }))
.filter((item) => item.role === Roles.Workspace.Guest)
.filter((item) => !seatTypeFilter.value || item.seatType === seatTypeFilter.value)
})
@@ -85,15 +85,13 @@
</template>
<script setup lang="ts">
import {
Roles,
type WorkspaceRoles,
type MaybeNullOrUndefined,
type WorkspaceSeatType
} from '@speckle/shared'
import { Roles, type WorkspaceRoles, type MaybeNullOrUndefined } from '@speckle/shared'
import { settingsWorkspacesMembersSearchQuery } from '~~/lib/settings/graphql/queries'
import { useQuery } from '@vue/apollo-composable'
import type { SettingsWorkspacesNewMembersTable_WorkspaceFragment } from '~~/lib/common/generated/gql/graphql'
import {
WorkspaceSeatType,
type SettingsWorkspacesNewMembersTable_WorkspaceFragment
} from '~~/lib/common/generated/gql/graphql'
import { graphql } from '~/lib/common/generated/gql'
import { ExclamationCircleIcon } from '@heroicons/vue/24/outline'
import { LearnMoreRolesSeatsUrl } from '~~/lib/common/helpers/route'
@@ -163,7 +161,7 @@ const members = computed(() => {
return (memberArray || [])
.map(({ user, seatType, ...rest }) => ({
...user,
seatType,
seatType: seatType || WorkspaceSeatType.Viewer,
...rest
}))
.filter((user) => user.role !== Roles.Workspace.Guest)
@@ -134,7 +134,7 @@ type Documents = {
"\n fragment SettingsWorkspacesMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n user {\n id\n avatar\n name\n company\n workspaceDomainPolicyCompliant\n }\n }\n": typeof types.SettingsWorkspacesMembersTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersTable_Workspace on Workspace {\n id\n name\n ...SettingsSharedDeleteUserDialog_Workspace\n ...SettingsWorkspacesMembersTableHeader_Workspace\n ...SettingsWorkspacesMembersChangeRoleDialog_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersTable_WorkspaceCollaborator\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...InviteDialogWorkspace_Workspace\n }\n": typeof types.SettingsWorkspacesMembersTableHeader_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n user {\n id\n avatar\n name\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersNewGuestsTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n ...SettingsSharedDeleteUserDialog_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaborator\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersNewGuestsTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesNewMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant\n }\n }\n": typeof types.SettingsWorkspacesNewMembersTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesNewMembersTable_Workspace on Workspace {\n id\n name\n ...SettingsSharedDeleteUserDialog_Workspace\n ...SettingsWorkspacesMembersTableHeader_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesNewMembersTable_WorkspaceCollaborator\n }\n }\n }\n": typeof types.SettingsWorkspacesNewMembersTable_WorkspaceFragmentDoc,
@@ -555,7 +555,7 @@ const documents: Documents = {
"\n fragment SettingsWorkspacesMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n user {\n id\n avatar\n name\n company\n workspaceDomainPolicyCompliant\n }\n }\n": types.SettingsWorkspacesMembersTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersTable_Workspace on Workspace {\n id\n name\n ...SettingsSharedDeleteUserDialog_Workspace\n ...SettingsWorkspacesMembersTableHeader_Workspace\n ...SettingsWorkspacesMembersChangeRoleDialog_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesMembersTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...InviteDialogWorkspace_Workspace\n }\n": types.SettingsWorkspacesMembersTableHeader_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n user {\n id\n avatar\n name\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n": types.SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n": types.SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersNewGuestsTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n ...SettingsSharedDeleteUserDialog_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesMembersNewGuestsTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesNewMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant\n }\n }\n": types.SettingsWorkspacesNewMembersTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesNewMembersTable_Workspace on Workspace {\n id\n name\n ...SettingsSharedDeleteUserDialog_Workspace\n ...SettingsWorkspacesMembersTableHeader_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesNewMembersTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesNewMembersTable_WorkspaceFragmentDoc,
@@ -1353,7 +1353,7 @@ export function graphql(source: "\n fragment SettingsWorkspacesMembersTableHead
/**
* 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 SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n user {\n id\n avatar\n name\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n user {\n id\n avatar\n name\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n"];
export function graphql(source: "\n fragment SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersNewGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\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
@@ -4,7 +4,21 @@ enum WorkspaceSeatType {
}
extend type WorkspaceCollaborator {
seatType: WorkspaceSeatType!
seatType: WorkspaceSeatType
}
extend type ProjectCollaborator {
"""
The collaborator's workspace seat type for the workspace this project is in
"""
seatType: WorkspaceSeatType
}
extend type Workspace {
"""
Active user's seat type for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you.
"""
seatType: WorkspaceSeatType
}
input WorkspaceUpdateSeatTypeInput {
+1
View File
@@ -40,6 +40,7 @@ generates:
Comment: '@/modules/comments/helpers/graphTypes#CommentGraphQLReturn'
PendingStreamCollaborator: '@/modules/serverinvites/helpers/graphTypes#PendingStreamCollaboratorGraphQLReturn'
StreamCollaborator: '@/modules/core/helpers/graphTypes#StreamCollaboratorGraphQLReturn'
ProjectCollaborator: '@/modules/core/helpers/graphTypes#ProjectCollaboratorGraphQLReturn'
FileUpload: '@/modules/fileuploads/helpers/types#FileUploadGraphQLReturn'
AutomateFunction: '@/modules/automate/helpers/graphTypes#AutomateFunctionGraphQLReturn'
AutomateFunctionRelease: '@/modules/automate/helpers/graphTypes#AutomateFunctionReleaseGraphQLReturn'
@@ -1,5 +1,5 @@
import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';
import { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, ObjectGraphQLReturn, VersionGraphQLReturn, ServerInviteGraphQLReturnType, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, MutationsObjectGraphQLReturn, LimitedUserGraphQLReturn, UserGraphQLReturn, GraphQLEmptyReturn, StreamCollaboratorGraphQLReturn, ServerInfoGraphQLReturn, BranchGraphQLReturn } from '@/modules/core/helpers/graphTypes';
import { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, ObjectGraphQLReturn, VersionGraphQLReturn, ServerInviteGraphQLReturnType, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, MutationsObjectGraphQLReturn, LimitedUserGraphQLReturn, UserGraphQLReturn, GraphQLEmptyReturn, StreamCollaboratorGraphQLReturn, ProjectCollaboratorGraphQLReturn, ServerInfoGraphQLReturn, BranchGraphQLReturn } from '@/modules/core/helpers/graphTypes';
import { StreamAccessRequestGraphQLReturn, ProjectAccessRequestGraphQLReturn } from '@/modules/accessrequests/helpers/graphTypes';
import { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn } from '@/modules/comments/helpers/graphTypes';
import { PendingStreamCollaboratorGraphQLReturn } from '@/modules/serverinvites/helpers/graphTypes';
@@ -2253,6 +2253,8 @@ export type ProjectCollaborator = {
__typename?: 'ProjectCollaborator';
id: Scalars['ID']['output'];
role: Scalars['String']['output'];
/** The collaborator's workspace seat type for the workspace this project is in */
seatType?: Maybe<WorkspaceSeatType>;
user: LimitedUser;
};
@@ -4350,6 +4352,8 @@ export type Workspace = {
readOnly: Scalars['Boolean']['output'];
/** Active user's role for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
role?: Maybe<Scalars['String']['output']>;
/** Active user's seat type for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
seatType?: Maybe<WorkspaceSeatType>;
slug: Scalars['String']['output'];
/** Information about the workspace's SSO configuration and the current user's SSO session, if present */
sso?: Maybe<WorkspaceSso>;
@@ -5141,7 +5145,7 @@ export type ResolversTypes = {
ProjectAutomationUpdateInput: ProjectAutomationUpdateInput;
ProjectAutomationsUpdatedMessage: ResolverTypeWrapper<ProjectAutomationsUpdatedMessageGraphQLReturn>;
ProjectAutomationsUpdatedMessageType: ProjectAutomationsUpdatedMessageType;
ProjectCollaborator: ResolverTypeWrapper<Omit<ProjectCollaborator, 'user'> & { user: ResolversTypes['LimitedUser'] }>;
ProjectCollaborator: ResolverTypeWrapper<ProjectCollaboratorGraphQLReturn>;
ProjectCollection: ResolverTypeWrapper<Omit<ProjectCollection, 'items'> & { items: Array<ResolversTypes['Project']> }>;
ProjectCommentCollection: ResolverTypeWrapper<Omit<ProjectCommentCollection, 'items'> & { items: Array<ResolversTypes['Comment']> }>;
ProjectCommentsFilter: ProjectCommentsFilter;
@@ -5453,7 +5457,7 @@ export type ResolversParentTypes = {
ProjectAutomationRevisionCreateInput: ProjectAutomationRevisionCreateInput;
ProjectAutomationUpdateInput: ProjectAutomationUpdateInput;
ProjectAutomationsUpdatedMessage: ProjectAutomationsUpdatedMessageGraphQLReturn;
ProjectCollaborator: Omit<ProjectCollaborator, 'user'> & { user: ResolversParentTypes['LimitedUser'] };
ProjectCollaborator: ProjectCollaboratorGraphQLReturn;
ProjectCollection: Omit<ProjectCollection, 'items'> & { items: Array<ResolversParentTypes['Project']> };
ProjectCommentCollection: Omit<ProjectCommentCollection, 'items'> & { items: Array<ResolversParentTypes['Comment']> };
ProjectCommentsFilter: ProjectCommentsFilter;
@@ -6417,6 +6421,7 @@ export type ProjectAutomationsUpdatedMessageResolvers<ContextType = GraphQLConte
export type ProjectCollaboratorResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ProjectCollaborator'] = ResolversParentTypes['ProjectCollaborator']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
role?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
seatType?: Resolver<Maybe<ResolversTypes['WorkspaceSeatType']>, ParentType, ContextType>;
user?: Resolver<ResolversTypes['LimitedUser'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@@ -7085,6 +7090,7 @@ export type WorkspaceResolvers<ContextType = GraphQLContext, ParentType extends
projects?: Resolver<ResolversTypes['ProjectCollection'], ParentType, ContextType, RequireFields<WorkspaceProjectsArgs, 'limit'>>;
readOnly?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
role?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
seatType?: Resolver<Maybe<ResolversTypes['WorkspaceSeatType']>, ParentType, ContextType>;
slug?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
sso?: Resolver<Maybe<ResolversTypes['WorkspaceSso']>, ParentType, ContextType>;
subscription?: Resolver<Maybe<ResolversTypes['WorkspaceSubscription']>, ParentType, ContextType>;
@@ -382,7 +382,8 @@ export = {
return users.map((u) => ({
user: u,
role: u.streamRole,
id: u.id
id: u.id,
projectId: parent.id
}))
},
async sourceApps(parent, _args, ctx) {
@@ -13,6 +13,7 @@ import { Roles, ServerRoles, StreamRoles } from '@/modules/core/helpers/mainCons
import {
BranchRecord,
CommitRecord,
LimitedUserRecord,
ObjectRecord,
ServerInfo,
StreamRecord,
@@ -128,3 +129,10 @@ export type StreamCollaboratorGraphQLReturn = {
}
export type ServerInfoGraphQLReturn = ServerInfo
export type ProjectCollaboratorGraphQLReturn = {
id: string
user: LimitedUserRecord
role: StreamRoles
projectId: string
}
@@ -2233,6 +2233,8 @@ export type ProjectCollaborator = {
__typename?: 'ProjectCollaborator';
id: Scalars['ID']['output'];
role: Scalars['String']['output'];
/** The collaborator's workspace seat type for the workspace this project is in */
seatType?: Maybe<WorkspaceSeatType>;
user: LimitedUser;
};
@@ -4330,6 +4332,8 @@ export type Workspace = {
readOnly: Scalars['Boolean']['output'];
/** Active user's role for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
role?: Maybe<Scalars['String']['output']>;
/** Active user's seat type for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
seatType?: Maybe<WorkspaceSeatType>;
slug: Scalars['String']['output'];
/** Information about the workspace's SSO configuration and the current user's SSO session, if present */
sso?: Maybe<WorkspaceSso>;
@@ -59,3 +59,25 @@ export type GetWorkspaceUserSeat = (params: {
workspaceId: string
userId: string
}) => Promise<Optional<WorkspaceSeat>>
export type GetWorkspacesUsersSeats = (params: {
requests: Array<{
userId: string
workspaceId: string
}>
}) => Promise<{
[workspaceId: string]: {
[userId: string]: WorkspaceSeat
}
}>
export type GetProjectsUsersSeats = (params: {
requests: Array<{
userId: string
projectId: string
}>
}) => Promise<{
[projectId: string]: {
[userId: string]: WorkspaceSeat
}
}>
@@ -0,0 +1,64 @@
import { WorkspaceSeat } from '@/modules/gatekeeper/domain/billing'
import {
getProjectsUsersSeatsFactory,
getWorkspacesUsersSeatsFactory
} from '@/modules/gatekeeper/repositories/workspaceSeat'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper'
const { FF_GATEKEEPER_MODULE_ENABLED } = getFeatureFlags()
declare module '@/modules/core/loaders' {
interface ModularizedDataLoaders
extends Partial<ReturnType<typeof dataLoadersDefinition>> {}
}
const dataLoadersDefinition = defineRequestDataloaders(
({ createLoader, deps: { db } }) => {
const getWorkspacesUsersSeats = getWorkspacesUsersSeatsFactory({ db })
const getProjectsUsersSeats = getProjectsUsersSeatsFactory({ db })
return {
gatekeeper: {
getUserWorkspaceSeat: createLoader<
{ workspaceId: string; userId: string },
WorkspaceSeat | null,
string
>(
async (requests) => {
const results = await getWorkspacesUsersSeats({
requests: requests.slice()
})
return requests.map(
({ workspaceId, userId }) => results[workspaceId]?.[userId] || null
)
},
{
cacheKeyFn: ({ workspaceId, userId }) => `${workspaceId}-${userId}`
}
),
getUserProjectSeat: createLoader<
{ projectId: string; userId: string },
WorkspaceSeat | null,
string
>(
async (requests) => {
const results = await getProjectsUsersSeats({
requests: requests.slice()
})
return requests.map(
({ projectId, userId }) => results[projectId]?.[userId] || null
)
},
{
cacheKeyFn: ({ projectId, userId }) => `${projectId}-${userId}`
}
)
}
}
}
)
export default FF_GATEKEEPER_MODULE_ENABLED ? dataLoadersDefinition : undefined
@@ -157,6 +157,17 @@ export = FF_GATEKEEPER_MODULE_ENABLED
return await isWorkspaceReadOnlyFactory({ getWorkspacePlan })({
workspaceId: parent.id
})
},
seatType: async (parent, _args, context) => {
if (!context.userId) return null
const seat = await context.loaders.gatekeeper!.getUserWorkspaceSeat.load({
workspaceId: parent.id,
userId: context.userId
})
// Defaults to Editor for old plans that don't have seat types
return seat?.type || WorkspaceSeatType.Editor
}
},
WorkspaceSubscription: {
@@ -200,9 +211,10 @@ export = FF_GATEKEEPER_MODULE_ENABLED
},
WorkspaceCollaborator: {
seatType: async (parent, _args, context) => {
const seat = await context.loaders
.gatekeeper!.getUserWorkspaceSeatType.forWorkspace(parent.workspaceId)
.load(parent.id)
const seat = await context.loaders.gatekeeper!.getUserWorkspaceSeat.load({
workspaceId: parent.workspaceId,
userId: parent.id
})
// Defaults to Editor for old plans that don't have seat types
return seat?.type || WorkspaceSeatType.Editor
@@ -224,6 +236,17 @@ export = FF_GATEKEEPER_MODULE_ENABLED
}))
}
},
ProjectCollaborator: {
seatType: async (parent, _args, context) => {
const seat = await context.loaders.gatekeeper!.getUserProjectSeat.load({
projectId: parent.projectId,
userId: parent.id
})
// Defaults to Editor for old plans that don't have seat types
return seat?.type || WorkspaceSeatType.Editor
}
},
WorkspaceMutations: {
billing: () => ({}),
updateSeatType: async (_parent, args, ctx) => {
@@ -1,4 +1,5 @@
import { buildTableHelper } from '@/modules/core/dbSchema'
import { buildTableHelper, StreamAcl, Streams } from '@/modules/core/dbSchema'
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
import {
GetWorkspaceRoleAndSeat,
GetWorkspaceRolesAndSeats,
@@ -8,6 +9,8 @@ import {
CountSeatsByTypeInWorkspace,
CreateWorkspaceSeat,
DeleteWorkspaceSeat,
GetProjectsUsersSeats,
GetWorkspacesUsersSeats,
GetWorkspaceUserSeat,
GetWorkspaceUserSeats
} from '@/modules/gatekeeper/domain/operations'
@@ -26,7 +29,9 @@ const WorkspaceSeats = buildTableHelper('workspace_seats', [
const tables = {
workspaceSeats: (db: Knex) => db<WorkspaceSeat>(WorkspaceSeats.name),
workspaceAcl: (db: Knex) => db<WorkspaceAclRecord>(WorkspaceAcl.name)
workspaceAcl: (db: Knex) => db<WorkspaceAclRecord>(WorkspaceAcl.name),
streamAcl: (db: Knex) => db<StreamAclRecord>(StreamAcl.name),
streams: (db: Knex) => db<StreamRecord>(Streams.name)
}
export const countSeatsByTypeInWorkspaceFactory =
@@ -136,3 +141,67 @@ export const getWorkspaceRoleAndSeatFactory =
})
return rolesAndSeats[userId]
}
export const getWorkspacesUsersSeatsFactory =
(deps: { db: Knex }): GetWorkspacesUsersSeats =>
async (params) => {
const { requests } = params
const q = tables.workspaceSeats(deps.db).whereIn(
[WorkspaceSeats.col.workspaceId, WorkspaceSeats.col.userId],
requests.map(({ userId, workspaceId }) => [workspaceId, userId])
)
const results = await q
return results.reduce((acc, seat) => {
const { userId, workspaceId } = seat
if (!acc[workspaceId]) {
acc[workspaceId] = {}
}
if (!acc[workspaceId][userId]) {
acc[workspaceId][userId] = seat
}
return acc
}, {} as Awaited<ReturnType<GetWorkspacesUsersSeats>>)
}
export const getProjectsUsersSeatsFactory =
(deps: { db: Knex }): GetProjectsUsersSeats =>
async (params: {
requests: Array<{
userId: string
projectId: string
}>
}) => {
const { requests } = params
const idPairs = requests.map(({ userId, projectId }) => [userId, projectId])
const q = tables
.streamAcl(deps.db)
.whereIn([StreamAcl.col.userId, StreamAcl.col.resourceId], idPairs)
.innerJoin(Streams.name, Streams.col.id, StreamAcl.col.resourceId)
.leftJoin(WorkspaceSeats.name, (j1) => {
j1.on(WorkspaceSeats.col.userId, StreamAcl.col.userId).andOn(
WorkspaceSeats.col.workspaceId,
Streams.col.workspaceId
)
})
.select<Array<StreamAclRecord & WorkspaceSeat>>([
...StreamAcl.cols,
...WorkspaceSeats.cols
])
const results = await q
return results.reduce((acc, row) => {
const { userId, resourceId: projectId } = row
if (!acc[projectId]) {
acc[projectId] = {}
}
if (!acc[projectId][userId]) {
acc[projectId][userId] = row
}
return acc
}, {} as Awaited<ReturnType<GetProjectsUsersSeats>>)
}
@@ -1,47 +0,0 @@
import { WorkspaceSeat } from '@/modules/gatekeeper/domain/billing'
import { getWorkspaceUserSeatsFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper'
import DataLoader from 'dataloader'
const { FF_GATEKEEPER_MODULE_ENABLED } = getFeatureFlags()
declare module '@/modules/core/loaders' {
interface ModularizedDataLoaders
extends Partial<ReturnType<typeof dataLoadersDefinition>> {}
}
const dataLoadersDefinition = defineRequestDataloaders(
({ createLoader, deps: { db } }) => {
const getUserSeats = getWorkspaceUserSeatsFactory({ db })
return {
gatekeeper: {
getUserWorkspaceSeatType: (() => {
type LoaderType = DataLoader<string, WorkspaceSeat | null>
const workspaceLoaders = new Map<string, LoaderType>()
return {
clearAll: () => workspaceLoaders.clear(),
forWorkspace(workspaceId: string): LoaderType {
let loader = workspaceLoaders.get(workspaceId)
if (!loader) {
loader = createLoader<string, WorkspaceSeat | null>(async (ids) => {
const results = await getUserSeats({
userIds: ids.slice(),
workspaceId
})
return ids.map((id) => results[id] || null)
})
workspaceLoaders.set(workspaceId, loader)
}
return loader
}
}
})()
}
}
}
)
export default FF_GATEKEEPER_MODULE_ENABLED ? dataLoadersDefinition : undefined
@@ -20,6 +20,11 @@ const resolvers: Resolvers = FF_GATEKEEPER_MODULE_ENABLED
throw new GatekeeperModuleDisabledError()
}
},
ProjectCollaborator: {
seatType: () => {
throw new GatekeeperModuleDisabledError()
}
},
ServerWorkspacesInfo: {
planPrices: () => []
}
+4 -4
View File
@@ -160,12 +160,12 @@ export const graphDataloadersBuilders = (): RequestDataLoadersBuilder<any>[] =>
const codeModuleDirs = fs.readdirSync(`${appRoot}/modules`)
codeModuleDirs.forEach((file) => {
if (!enabledModuleNames.includes(file)) return
const fullPath = path.join(`${appRoot}/modules`, file)
const modulePath = path.join(`${appRoot}/modules`, file)
// load dataloaders
const directivesPath = path.join(fullPath, 'graph', 'dataloaders')
if (fs.existsSync(directivesPath)) {
const newLoaders = values(autoloadFromDirectory(directivesPath))
const fullPath = path.join(modulePath, 'graph', 'dataloaders')
if (fs.existsSync(fullPath)) {
const newLoaders = values(autoloadFromDirectory(fullPath))
.map((l) => l.default)
.filter(isNonNullable)
+1 -1
View File
@@ -6,7 +6,7 @@ const { getLogger, extendLoggerComponent } = Observability
export const logger = getLogger(
process.env.LOG_LEVEL || 'info',
process.env.LOG_PRETTY === 'true'
process.env.LOG_PRETTY === 'true' && !process.env.FORCE_NO_PRETTY
)
// loggers for phases of operation
export const startupLogger = logger.child({ phase: 'startup' })
@@ -2234,6 +2234,8 @@ export type ProjectCollaborator = {
__typename?: 'ProjectCollaborator';
id: Scalars['ID']['output'];
role: Scalars['String']['output'];
/** The collaborator's workspace seat type for the workspace this project is in */
seatType?: Maybe<WorkspaceSeatType>;
user: LimitedUser;
};
@@ -4331,6 +4333,8 @@ export type Workspace = {
readOnly: Scalars['Boolean']['output'];
/** Active user's role for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
role?: Maybe<Scalars['String']['output']>;
/** Active user's seat type for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
seatType?: Maybe<WorkspaceSeatType>;
slug: Scalars['String']['output'];
/** Information about the workspace's SSO configuration and the current user's SSO session, if present */
sso?: Maybe<WorkspaceSso>;