From da49ec662507ff6398f47ccb9a7f5915ce679c24 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Tue, 17 Dec 2024 12:25:23 +0100 Subject: [PATCH 01/21] feat(core): user projects filterable by workspace --- .../assets/core/typedefs/projects.graphql | 1 + .../modules/core/domain/streams/operations.ts | 2 +- .../modules/core/graph/generated/graphql.ts | 1 + .../modules/core/graph/resolvers/projects.ts | 9 +- .../modules/core/repositories/streams.ts | 2 +- .../modules/core/tests/helpers/graphql.ts | 16 +- .../tests/integration/projects.graph.spec.ts | 169 ++++++++++++++++++ .../graph/generated/graphql.ts | 1 + .../server/test/graphql/generated/graphql.ts | 9 +- 9 files changed, 194 insertions(+), 16 deletions(-) create mode 100644 packages/server/modules/core/tests/integration/projects.graph.spec.ts diff --git a/packages/server/assets/core/typedefs/projects.graphql b/packages/server/assets/core/typedefs/projects.graphql index 3cf504e2f..aeecf2bb5 100644 --- a/packages/server/assets/core/typedefs/projects.graphql +++ b/packages/server/assets/core/typedefs/projects.graphql @@ -199,6 +199,7 @@ input UserProjectsFilter { Only include projects where user has the specified roles """ onlyWithRoles: [String!] + workspaceId: ID } enum UserProjectsUpdatedMessageType { diff --git a/packages/server/modules/core/domain/streams/operations.ts b/packages/server/modules/core/domain/streams/operations.ts index 055066a35..f5d491701 100644 --- a/packages/server/modules/core/domain/streams/operations.ts +++ b/packages/server/modules/core/domain/streams/operations.ts @@ -163,7 +163,7 @@ export type BaseUserStreamsQueryParams = { * Only allow streams with the specified IDs to be returned */ streamIdWhitelist?: string[] - workspaceId?: string + workspaceId?: string | null /** * Only with active sso session diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 772658e78..d59436f14 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -3871,6 +3871,7 @@ export type UserProjectsFilter = { onlyWithRoles?: InputMaybe>; /** Filter out projects by name */ search?: InputMaybe; + workspaceId?: InputMaybe; }; export type UserProjectsUpdatedMessage = { diff --git a/packages/server/modules/core/graph/resolvers/projects.ts b/packages/server/modules/core/graph/resolvers/projects.ts index fc6bc83ae..a9185adf5 100644 --- a/packages/server/modules/core/graph/resolvers/projects.ts +++ b/packages/server/modules/core/graph/resolvers/projects.ts @@ -330,7 +330,8 @@ export = { forOtherUser: false, searchQuery: args.filter?.search || undefined, withRoles: (args.filter?.onlyWithRoles || []) as StreamRoles[], - streamIdWhitelist: toProjectIdWhitelist(ctx.resourceAccessRules) + streamIdWhitelist: toProjectIdWhitelist(ctx.resourceAccessRules), + workspaceId: args.filter?.workspaceId }), getUserStreamsCount({ userId: ctx.userId!, @@ -338,7 +339,8 @@ export = { searchQuery: args.filter?.search || undefined, withRoles: (args.filter?.onlyWithRoles || []) as StreamRoles[], streamIdWhitelist: toProjectIdWhitelist(ctx.resourceAccessRules), - onlyWithActiveSsoSession: true + onlyWithActiveSsoSession: true, + workspaceId: args.filter?.workspaceId }), getUserStreams({ userId: ctx.userId!, @@ -348,7 +350,8 @@ export = { forOtherUser: false, withRoles: (args.filter?.onlyWithRoles || []) as StreamRoles[], streamIdWhitelist: toProjectIdWhitelist(ctx.resourceAccessRules), - onlyWithActiveSsoSession: true + onlyWithActiveSsoSession: true, + workspaceId: args.filter?.workspaceId }) ]) diff --git a/packages/server/modules/core/repositories/streams.ts b/packages/server/modules/core/repositories/streams.ts index a2cbf98e6..02c364b64 100644 --- a/packages/server/modules/core/repositories/streams.ts +++ b/packages/server/modules/core/repositories/streams.ts @@ -713,7 +713,7 @@ const getUserStreamsQueryBaseFactory = }) } - if (workspaceId) { + if (!isUndefined(workspaceId)) { query.andWhere(Streams.col.workspaceId, workspaceId) } diff --git a/packages/server/modules/core/tests/helpers/graphql.ts b/packages/server/modules/core/tests/helpers/graphql.ts index 5043845c0..e130fd603 100644 --- a/packages/server/modules/core/tests/helpers/graphql.ts +++ b/packages/server/modules/core/tests/helpers/graphql.ts @@ -119,13 +119,15 @@ export const onBranchDeletedSubscription = gql` } ` -export const usersRetrievalQuery = gql` - query UsersRetrieval($input: UsersRetrievalInput!) { - users(input: $input) { - cursor - items { - id - name +export const activeUserProjectsQuery = gql` + query ActiveUserProjects($filter: UserProjectsFilter!) { + activeUser { + projects(filter: $filter) { + cursor + items { + id + name + } } } } diff --git a/packages/server/modules/core/tests/integration/projects.graph.spec.ts b/packages/server/modules/core/tests/integration/projects.graph.spec.ts new file mode 100644 index 000000000..3cfbab7e4 --- /dev/null +++ b/packages/server/modules/core/tests/integration/projects.graph.spec.ts @@ -0,0 +1,169 @@ +import { createTestWorkspace } from '@/modules/workspaces/tests/helpers/creation' +import { + BasicTestUser, + createAuthTokenForUser, + createTestUsers +} from '@/test/authHelper' +import { + ActiveUserProjectsDocument, + CreateProjectDocument, + CreateWorkspaceProjectDocument, + GetWorkspaceDocument +} from '@/test/graphql/generated/graphql' +import { + createTestContext, + testApolloServer, + TestApolloServer +} from '@/test/graphqlHelper' +import { beforeEachContext } from '@/test/hooks' +import { AllScopes, Roles } from '@speckle/shared' +import { expect } from 'chai' +import cryptoRandomString from 'crypto-random-string' +import { beforeEach } from 'mocha' + +describe('Projects GraphQL @core', () => { + let apollo: TestApolloServer + + const testAdminUser: BasicTestUser = { + id: '', + name: 'John Speckle', + email: 'john-speckle@example.org', + role: Roles.Server.Admin, + verified: true + } + + beforeEach(async () => { + await beforeEachContext() + await createTestUsers([testAdminUser]) + const token = await createAuthTokenForUser(testAdminUser.id, AllScopes) + apollo = await testApolloServer({ + context: await createTestContext({ + auth: true, + userId: testAdminUser.id, + token, + role: testAdminUser.role, + scopes: AllScopes + }) + }) + }) + + describe.only('query user.projects', () => { + it('should return projects with workspaceId=null', async () => { + const workspace = { + id: '', + name: 'test ws', + slug: cryptoRandomString({ length: 10 }), + ownerId: '' + } + await createTestWorkspace(workspace, testAdminUser) + + const getWorkspaceRes = await apollo.execute(GetWorkspaceDocument, { + workspaceId: workspace.id + }) + + expect(getWorkspaceRes).to.not.haveGraphQLErrors() + const workspaceId = getWorkspaceRes.data!.workspace.id + + const createProjectInWorkspaceRes = await apollo.execute( + CreateWorkspaceProjectDocument, + { input: { name: 'project', workspaceId } } + ) + expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors() + + const createProjectNonInWorkspaceRes = await apollo.execute( + CreateProjectDocument, + { input: { name: 'project' } } + ) + expect(createProjectNonInWorkspaceRes).to.not.haveGraphQLErrors() + const projectNonInWorkspace = + createProjectNonInWorkspaceRes.data!.projectMutations.create + + const userProjectsRes = await apollo.execute(ActiveUserProjectsDocument, { + filter: { workspaceId: null } + }) + expect(userProjectsRes).to.not.haveGraphQLErrors() + + const nonWorkspaceProjects = userProjectsRes.data!.activeUser!.projects.items + + expect(nonWorkspaceProjects).to.have.length(1) + expect(nonWorkspaceProjects[0].id).to.eq(projectNonInWorkspace.id) + }) + it('should return projects in workspace', async () => { + const workspace = { + id: '', + name: 'test ws', + slug: cryptoRandomString({ length: 10 }), + ownerId: '' + } + await createTestWorkspace(workspace, testAdminUser) + + const getWorkspaceRes = await apollo.execute(GetWorkspaceDocument, { + workspaceId: workspace.id + }) + + expect(getWorkspaceRes).to.not.haveGraphQLErrors() + const workspaceId = getWorkspaceRes.data!.workspace.id + + const createProjectInWorkspaceRes = await apollo.execute( + CreateWorkspaceProjectDocument, + { input: { name: 'project', workspaceId } } + ) + expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors() + const projectInWorkspace = + createProjectInWorkspaceRes.data!.workspaceMutations.projects.create + + const createProjectNonInWorkspaceRes = await apollo.execute( + CreateProjectDocument, + { input: { name: 'project' } } + ) + expect(createProjectNonInWorkspaceRes).to.not.haveGraphQLErrors() + + const userProjectsRes = await apollo.execute(ActiveUserProjectsDocument, { + filter: { workspaceId } + }) + expect(userProjectsRes).to.not.haveGraphQLErrors() + + const nonWorkspaceProjects = userProjectsRes.data!.activeUser!.projects.items + + expect(nonWorkspaceProjects).to.have.length(1) + expect(nonWorkspaceProjects[0].id).to.eq(projectInWorkspace.id) + }) + it('should return all user projects', async () => { + const workspace = { + id: '', + name: 'test ws', + slug: cryptoRandomString({ length: 10 }), + ownerId: '' + } + await createTestWorkspace(workspace, testAdminUser) + + const getWorkspaceRes = await apollo.execute(GetWorkspaceDocument, { + workspaceId: workspace.id + }) + + expect(getWorkspaceRes).to.not.haveGraphQLErrors() + const workspaceId = getWorkspaceRes.data!.workspace.id + + const createProjectInWorkspaceRes = await apollo.execute( + CreateWorkspaceProjectDocument, + { input: { name: 'project', workspaceId } } + ) + expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors() + + const createProjectNonInWorkspaceRes = await apollo.execute( + CreateProjectDocument, + { input: { name: 'project' } } + ) + expect(createProjectNonInWorkspaceRes).to.not.haveGraphQLErrors() + + const userProjectsRes = await apollo.execute(ActiveUserProjectsDocument, { + filter: {} + }) + expect(userProjectsRes).to.not.haveGraphQLErrors() + + const nonWorkspaceProjects = userProjectsRes.data!.activeUser!.projects.items + + expect(nonWorkspaceProjects).to.have.length(2) + }) + }) +}) 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 93d4f6e96..382fda94a 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -3852,6 +3852,7 @@ export type UserProjectsFilter = { onlyWithRoles?: InputMaybe>; /** Filter out projects by name */ search?: InputMaybe; + workspaceId?: InputMaybe; }; export type UserProjectsUpdatedMessage = { diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index 2b975f722..8445cdd56 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -3853,6 +3853,7 @@ export type UserProjectsFilter = { onlyWithRoles?: InputMaybe>; /** Filter out projects by name */ search?: InputMaybe; + workspaceId?: InputMaybe; }; export type UserProjectsUpdatedMessage = { @@ -4694,12 +4695,12 @@ export type OnBranchDeletedSubscriptionVariables = Exact<{ export type OnBranchDeletedSubscription = { __typename?: 'Subscription', branchDeleted?: Record | null }; -export type UsersRetrievalQueryVariables = Exact<{ - input: UsersRetrievalInput; +export type ActiveUserProjectsQueryVariables = Exact<{ + filter: UserProjectsFilter; }>; -export type UsersRetrievalQuery = { __typename?: 'Query', users: { __typename?: 'UserSearchResultCollection', cursor?: string | null, items: Array<{ __typename?: 'LimitedUser', id: string, name: string }> } }; +export type ActiveUserProjectsQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', projects: { __typename?: 'UserProjectCollection', cursor?: string | null, items: Array<{ __typename?: 'Project', id: string, name: string }> } } | null }; export type BasicWorkspaceFragment = { __typename?: 'Workspace', id: string, name: string, slug: string, updatedAt: string, createdAt: string, role?: string | null }; @@ -5521,7 +5522,7 @@ export const OnProjectModelsUpdatedDocument = {"kind":"Document","definitions":[ export const OnBranchCreatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnBranchCreated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"branchCreated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}]}]}}]} as unknown as DocumentNode; export const OnBranchUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnBranchUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"branchId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"branchUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}},{"kind":"Argument","name":{"kind":"Name","value":"branchId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"branchId"}}}]}]}}]} as unknown as DocumentNode; export const OnBranchDeletedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnBranchDeleted"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"branchDeleted"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}]}]}}]} as unknown as DocumentNode; -export const UsersRetrievalDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UsersRetrieval"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UsersRetrievalInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; +export const ActiveUserProjectsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActiveUserProjects"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserProjectsFilter"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateWorkspaceInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateWorkspaceInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceInviteCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"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":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","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":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"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"}}]}},{"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":"token"}}]}}]} as unknown as DocumentNode; export const BatchCreateWorkspaceInvitesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"BatchCreateWorkspaceInvites"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceInviteCreateInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"batchCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"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":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","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":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"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"}}]}},{"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":"token"}}]}}]} as unknown as DocumentNode; export const GetWorkspaceWithTeamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceWithTeam"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","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":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"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"}}]}},{"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":"token"}}]}}]} as unknown as DocumentNode; From b1e483462c06e8c1b5b155978ca17b944fd1bc47 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Fri, 20 Dec 2024 10:44:58 +0100 Subject: [PATCH 02/21] feat(workspaces): create workspace_join_requests table and domain types --- .../modules/workspacesCore/domain/types.ts | 14 +++++++ .../modules/workspacesCore/helpers/db.ts | 8 ++++ ...08_create_workspace_join_requests_table.ts | 37 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 packages/server/modules/workspacesCore/migrations/20241220093308_create_workspace_join_requests_table.ts diff --git a/packages/server/modules/workspacesCore/domain/types.ts b/packages/server/modules/workspacesCore/domain/types.ts index d05118278..65d4ff5f9 100644 --- a/packages/server/modules/workspacesCore/domain/types.ts +++ b/packages/server/modules/workspacesCore/domain/types.ts @@ -66,3 +66,17 @@ export type WorkspaceRegionAssignment = { regionKey: string createdAt: Date } + +export type WorkspaceJoinRequestStatus = + | 'pending' + | 'accepted' + | 'rejected' + | 'dismissed' + +export type WorkspaceJoinRequest = { + workspaceId: string + userId: string + status: WorkspaceJoinRequestStatus + createdAt: Date + updatedAt: Date +} diff --git a/packages/server/modules/workspacesCore/helpers/db.ts b/packages/server/modules/workspacesCore/helpers/db.ts index dc0321614..1836d12bc 100644 --- a/packages/server/modules/workspacesCore/helpers/db.ts +++ b/packages/server/modules/workspacesCore/helpers/db.ts @@ -30,3 +30,11 @@ export const WorkspaceDomains = buildTableHelper('workspace_domains', [ 'createdByUserId', 'verified' ]) + +export const WorkspaceJoinRequests = buildTableHelper('workspace_join_requests', [ + 'workspaceId', + 'userId', + 'status', + 'createdAt', + 'updatedAt' +]) diff --git a/packages/server/modules/workspacesCore/migrations/20241220093308_create_workspace_join_requests_table.ts b/packages/server/modules/workspacesCore/migrations/20241220093308_create_workspace_join_requests_table.ts new file mode 100644 index 000000000..ce94b464a --- /dev/null +++ b/packages/server/modules/workspacesCore/migrations/20241220093308_create_workspace_join_requests_table.ts @@ -0,0 +1,37 @@ +import { Knex } from 'knex' + +const USERS_TABLE = 'users' +const WORKSPACES_TABLE = 'workspaces' +const WORKSPACE_JOIN_REQUESTS_TABLE = 'workspace_join_requests' + +export async function up(knex: Knex): Promise { + await knex.schema.createTable(WORKSPACE_JOIN_REQUESTS_TABLE, (table) => { + table + .text('workspaceId') + .references('id') + .inTable(WORKSPACES_TABLE) + .onDelete('cascade') + .notNullable() + table + .text('userId') + .references('id') + .inTable(USERS_TABLE) + .onDelete('cascade') + .notNullable() + table.text('status').notNullable() + table + .timestamp('createdAt', { precision: 3, useTz: true }) + .defaultTo(knex.fn.now()) + .notNullable() + table + .timestamp('updatedAt', { precision: 3, useTz: true }) + .defaultTo(knex.fn.now()) + .notNullable() + + table.primary(['workspaceId', 'userId']) + }) +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTable(WORKSPACE_JOIN_REQUESTS_TABLE) +} From efdc53a5f6926a4dba5978d9ccb14bf9bec709a5 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Mon, 30 Dec 2024 12:42:16 +0100 Subject: [PATCH 03/21] feat(gatekeeper): fix tests --- packages/server/modules/core/tests/helpers/graphql.ts | 11 +++++++++++ .../core/tests/integration/projects.graph.spec.ts | 2 +- packages/server/test/graphql/generated/graphql.ts | 8 ++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/server/modules/core/tests/helpers/graphql.ts b/packages/server/modules/core/tests/helpers/graphql.ts index e130fd603..6e7515990 100644 --- a/packages/server/modules/core/tests/helpers/graphql.ts +++ b/packages/server/modules/core/tests/helpers/graphql.ts @@ -118,6 +118,17 @@ export const onBranchDeletedSubscription = gql` branchDeleted(streamId: $streamId) } ` +export const usersRetrievalQuery = gql` + query UsersRetrieval($input: UsersRetrievalInput!) { + users(input: $input) { + cursor + items { + id + name + } + } + } +` export const activeUserProjectsQuery = gql` query ActiveUserProjects($filter: UserProjectsFilter!) { diff --git a/packages/server/modules/core/tests/integration/projects.graph.spec.ts b/packages/server/modules/core/tests/integration/projects.graph.spec.ts index 3cfbab7e4..4a65cd32e 100644 --- a/packages/server/modules/core/tests/integration/projects.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/projects.graph.spec.ts @@ -47,7 +47,7 @@ describe('Projects GraphQL @core', () => { }) }) - describe.only('query user.projects', () => { + describe('query user.projects', () => { it('should return projects with workspaceId=null', async () => { const workspace = { id: '', diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index fcd0b5759..53d0666d0 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -4697,6 +4697,13 @@ export type OnBranchDeletedSubscriptionVariables = Exact<{ export type OnBranchDeletedSubscription = { __typename?: 'Subscription', branchDeleted?: Record | null }; +export type UsersRetrievalQueryVariables = Exact<{ + input: UsersRetrievalInput; +}>; + + +export type UsersRetrievalQuery = { __typename?: 'Query', users: { __typename?: 'UserSearchResultCollection', cursor?: string | null, items: Array<{ __typename?: 'LimitedUser', id: string, name: string }> } }; + export type ActiveUserProjectsQueryVariables = Exact<{ filter: UserProjectsFilter; }>; @@ -5524,6 +5531,7 @@ export const OnProjectModelsUpdatedDocument = {"kind":"Document","definitions":[ export const OnBranchCreatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnBranchCreated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"branchCreated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}]}]}}]} as unknown as DocumentNode; export const OnBranchUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnBranchUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"branchId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"branchUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}},{"kind":"Argument","name":{"kind":"Name","value":"branchId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"branchId"}}}]}]}}]} as unknown as DocumentNode; export const OnBranchDeletedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnBranchDeleted"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"branchDeleted"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}]}]}}]} as unknown as DocumentNode; +export const UsersRetrievalDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UsersRetrieval"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UsersRetrievalInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; export const ActiveUserProjectsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActiveUserProjects"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserProjectsFilter"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateWorkspaceInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateWorkspaceInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceInviteCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"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":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","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":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"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"}}]}},{"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":"token"}}]}}]} as unknown as DocumentNode; export const BatchCreateWorkspaceInvitesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"BatchCreateWorkspaceInvites"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceInviteCreateInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"batchCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"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":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","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":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"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"}}]}},{"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":"token"}}]}}]} as unknown as DocumentNode; From b98a081157028107be998a216a3f0276a30f3b2b Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Mon, 30 Dec 2024 18:03:38 +0100 Subject: [PATCH 04/21] feat(gatekeeper): disable tests when workspace module is disabled --- .../tests/integration/projects.graph.spec.ts | 214 +++++++++--------- 1 file changed, 113 insertions(+), 101 deletions(-) diff --git a/packages/server/modules/core/tests/integration/projects.graph.spec.ts b/packages/server/modules/core/tests/integration/projects.graph.spec.ts index 4a65cd32e..de7ad6c3d 100644 --- a/packages/server/modules/core/tests/integration/projects.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/projects.graph.spec.ts @@ -16,10 +16,13 @@ import { TestApolloServer } from '@/test/graphqlHelper' import { beforeEachContext } from '@/test/hooks' -import { AllScopes, Roles } from '@speckle/shared' +import { Roles, AllScopes } from '@/modules/core/helpers/mainConstants' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' import { beforeEach } from 'mocha' +import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' + +const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() describe('Projects GraphQL @core', () => { let apollo: TestApolloServer @@ -48,122 +51,131 @@ describe('Projects GraphQL @core', () => { }) describe('query user.projects', () => { - it('should return projects with workspaceId=null', async () => { - const workspace = { - id: '', - name: 'test ws', - slug: cryptoRandomString({ length: 10 }), - ownerId: '' + ;(FF_WORKSPACES_MODULE_ENABLED ? it : it.skip)( + 'should return projects with workspaceId=null', + async () => { + const workspace = { + id: '', + name: 'test ws', + slug: cryptoRandomString({ length: 10 }), + ownerId: '' + } + await createTestWorkspace(workspace, testAdminUser) + + const getWorkspaceRes = await apollo.execute(GetWorkspaceDocument, { + workspaceId: workspace.id + }) + + expect(getWorkspaceRes).to.not.haveGraphQLErrors() + const workspaceId = getWorkspaceRes.data!.workspace.id + + const createProjectInWorkspaceRes = await apollo.execute( + CreateWorkspaceProjectDocument, + { input: { name: 'project', workspaceId } } + ) + expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors() + + const createProjectNonInWorkspaceRes = await apollo.execute( + CreateProjectDocument, + { input: { name: 'project' } } + ) + expect(createProjectNonInWorkspaceRes).to.not.haveGraphQLErrors() + const projectNonInWorkspace = + createProjectNonInWorkspaceRes.data!.projectMutations.create + + const userProjectsRes = await apollo.execute(ActiveUserProjectsDocument, { + filter: { workspaceId: null } + }) + expect(userProjectsRes).to.not.haveGraphQLErrors() + + const nonWorkspaceProjects = userProjectsRes.data!.activeUser!.projects.items + + expect(nonWorkspaceProjects).to.have.length(1) + expect(nonWorkspaceProjects[0].id).to.eq(projectNonInWorkspace.id) } - await createTestWorkspace(workspace, testAdminUser) + ) + ;(FF_WORKSPACES_MODULE_ENABLED ? it : it.skip)( + 'should return projects in workspace', + async () => { + const workspace = { + id: '', + name: 'test ws', + slug: cryptoRandomString({ length: 10 }), + ownerId: '' + } + await createTestWorkspace(workspace, testAdminUser) - const getWorkspaceRes = await apollo.execute(GetWorkspaceDocument, { - workspaceId: workspace.id - }) + const getWorkspaceRes = await apollo.execute(GetWorkspaceDocument, { + workspaceId: workspace.id + }) - expect(getWorkspaceRes).to.not.haveGraphQLErrors() - const workspaceId = getWorkspaceRes.data!.workspace.id + expect(getWorkspaceRes).to.not.haveGraphQLErrors() + const workspaceId = getWorkspaceRes.data!.workspace.id - const createProjectInWorkspaceRes = await apollo.execute( - CreateWorkspaceProjectDocument, - { input: { name: 'project', workspaceId } } - ) - expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors() + const createProjectInWorkspaceRes = await apollo.execute( + CreateWorkspaceProjectDocument, + { input: { name: 'project', workspaceId } } + ) + expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors() + const projectInWorkspace = + createProjectInWorkspaceRes.data!.workspaceMutations.projects.create - const createProjectNonInWorkspaceRes = await apollo.execute( - CreateProjectDocument, - { input: { name: 'project' } } - ) - expect(createProjectNonInWorkspaceRes).to.not.haveGraphQLErrors() - const projectNonInWorkspace = - createProjectNonInWorkspaceRes.data!.projectMutations.create + const createProjectNonInWorkspaceRes = await apollo.execute( + CreateProjectDocument, + { input: { name: 'project' } } + ) + expect(createProjectNonInWorkspaceRes).to.not.haveGraphQLErrors() - const userProjectsRes = await apollo.execute(ActiveUserProjectsDocument, { - filter: { workspaceId: null } - }) - expect(userProjectsRes).to.not.haveGraphQLErrors() + const userProjectsRes = await apollo.execute(ActiveUserProjectsDocument, { + filter: { workspaceId } + }) + expect(userProjectsRes).to.not.haveGraphQLErrors() - const nonWorkspaceProjects = userProjectsRes.data!.activeUser!.projects.items + const nonWorkspaceProjects = userProjectsRes.data!.activeUser!.projects.items - expect(nonWorkspaceProjects).to.have.length(1) - expect(nonWorkspaceProjects[0].id).to.eq(projectNonInWorkspace.id) - }) - it('should return projects in workspace', async () => { - const workspace = { - id: '', - name: 'test ws', - slug: cryptoRandomString({ length: 10 }), - ownerId: '' + expect(nonWorkspaceProjects).to.have.length(1) + expect(nonWorkspaceProjects[0].id).to.eq(projectInWorkspace.id) } - await createTestWorkspace(workspace, testAdminUser) + ) + ;(FF_WORKSPACES_MODULE_ENABLED ? it : it.skip)( + 'should return all user projects', + async () => { + const workspace = { + id: '', + name: 'test ws', + slug: cryptoRandomString({ length: 10 }), + ownerId: '' + } + await createTestWorkspace(workspace, testAdminUser) - const getWorkspaceRes = await apollo.execute(GetWorkspaceDocument, { - workspaceId: workspace.id - }) + const getWorkspaceRes = await apollo.execute(GetWorkspaceDocument, { + workspaceId: workspace.id + }) - expect(getWorkspaceRes).to.not.haveGraphQLErrors() - const workspaceId = getWorkspaceRes.data!.workspace.id + expect(getWorkspaceRes).to.not.haveGraphQLErrors() + const workspaceId = getWorkspaceRes.data!.workspace.id - const createProjectInWorkspaceRes = await apollo.execute( - CreateWorkspaceProjectDocument, - { input: { name: 'project', workspaceId } } - ) - expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors() - const projectInWorkspace = - createProjectInWorkspaceRes.data!.workspaceMutations.projects.create + const createProjectInWorkspaceRes = await apollo.execute( + CreateWorkspaceProjectDocument, + { input: { name: 'project', workspaceId } } + ) + expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors() - const createProjectNonInWorkspaceRes = await apollo.execute( - CreateProjectDocument, - { input: { name: 'project' } } - ) - expect(createProjectNonInWorkspaceRes).to.not.haveGraphQLErrors() + const createProjectNonInWorkspaceRes = await apollo.execute( + CreateProjectDocument, + { input: { name: 'project' } } + ) + expect(createProjectNonInWorkspaceRes).to.not.haveGraphQLErrors() - const userProjectsRes = await apollo.execute(ActiveUserProjectsDocument, { - filter: { workspaceId } - }) - expect(userProjectsRes).to.not.haveGraphQLErrors() + const userProjectsRes = await apollo.execute(ActiveUserProjectsDocument, { + filter: {} + }) + expect(userProjectsRes).to.not.haveGraphQLErrors() - const nonWorkspaceProjects = userProjectsRes.data!.activeUser!.projects.items + const nonWorkspaceProjects = userProjectsRes.data!.activeUser!.projects.items - expect(nonWorkspaceProjects).to.have.length(1) - expect(nonWorkspaceProjects[0].id).to.eq(projectInWorkspace.id) - }) - it('should return all user projects', async () => { - const workspace = { - id: '', - name: 'test ws', - slug: cryptoRandomString({ length: 10 }), - ownerId: '' + expect(nonWorkspaceProjects).to.have.length(2) } - await createTestWorkspace(workspace, testAdminUser) - - const getWorkspaceRes = await apollo.execute(GetWorkspaceDocument, { - workspaceId: workspace.id - }) - - expect(getWorkspaceRes).to.not.haveGraphQLErrors() - const workspaceId = getWorkspaceRes.data!.workspace.id - - const createProjectInWorkspaceRes = await apollo.execute( - CreateWorkspaceProjectDocument, - { input: { name: 'project', workspaceId } } - ) - expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors() - - const createProjectNonInWorkspaceRes = await apollo.execute( - CreateProjectDocument, - { input: { name: 'project' } } - ) - expect(createProjectNonInWorkspaceRes).to.not.haveGraphQLErrors() - - const userProjectsRes = await apollo.execute(ActiveUserProjectsDocument, { - filter: {} - }) - expect(userProjectsRes).to.not.haveGraphQLErrors() - - const nonWorkspaceProjects = userProjectsRes.data!.activeUser!.projects.items - - expect(nonWorkspaceProjects).to.have.length(2) - }) + ) }) }) From 01cb1b5eaf1c541ec794f17bb11928dc3ecf17e4 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Tue, 7 Jan 2025 17:56:08 +0100 Subject: [PATCH 05/21] feat(workspaces): test clarifications --- .../tests/integration/projects.graph.spec.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/server/modules/core/tests/integration/projects.graph.spec.ts b/packages/server/modules/core/tests/integration/projects.graph.spec.ts index de7ad6c3d..b21bd527e 100644 --- a/packages/server/modules/core/tests/integration/projects.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/projects.graph.spec.ts @@ -52,7 +52,7 @@ describe('Projects GraphQL @core', () => { describe('query user.projects', () => { ;(FF_WORKSPACES_MODULE_ENABLED ? it : it.skip)( - 'should return projects with workspaceId=null', + 'should return projects not in a workspace', async () => { const workspace = { id: '', @@ -88,10 +88,10 @@ describe('Projects GraphQL @core', () => { }) expect(userProjectsRes).to.not.haveGraphQLErrors() - const nonWorkspaceProjects = userProjectsRes.data!.activeUser!.projects.items + const projects = userProjectsRes.data!.activeUser!.projects.items - expect(nonWorkspaceProjects).to.have.length(1) - expect(nonWorkspaceProjects[0].id).to.eq(projectNonInWorkspace.id) + expect(projects).to.have.length(1) + expect(projects[0].id).to.eq(projectNonInWorkspace.id) } ) ;(FF_WORKSPACES_MODULE_ENABLED ? it : it.skip)( @@ -131,10 +131,10 @@ describe('Projects GraphQL @core', () => { }) expect(userProjectsRes).to.not.haveGraphQLErrors() - const nonWorkspaceProjects = userProjectsRes.data!.activeUser!.projects.items + const projects = userProjectsRes.data!.activeUser!.projects.items - expect(nonWorkspaceProjects).to.have.length(1) - expect(nonWorkspaceProjects[0].id).to.eq(projectInWorkspace.id) + expect(projects).to.have.length(1) + expect(projects[0].id).to.eq(projectInWorkspace.id) } ) ;(FF_WORKSPACES_MODULE_ENABLED ? it : it.skip)( @@ -172,9 +172,9 @@ describe('Projects GraphQL @core', () => { }) expect(userProjectsRes).to.not.haveGraphQLErrors() - const nonWorkspaceProjects = userProjectsRes.data!.activeUser!.projects.items + const projects = userProjectsRes.data!.activeUser!.projects.items - expect(nonWorkspaceProjects).to.have.length(2) + expect(projects).to.have.length(2) } ) }) From 0fa4e2f7ab6b6ae54107246b2b8b14118dae39cb Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 7 Jan 2025 21:09:52 +0100 Subject: [PATCH 06/21] Fix: Order server permission list ABC (#3774) --- .../frontend-2/lib/common/composables/serverInfo.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/frontend-2/lib/common/composables/serverInfo.ts b/packages/frontend-2/lib/common/composables/serverInfo.ts index 85f39fd3b..36506baab 100644 --- a/packages/frontend-2/lib/common/composables/serverInfo.ts +++ b/packages/frontend-2/lib/common/composables/serverInfo.ts @@ -27,10 +27,12 @@ export const useServerInfoScopes = () => { const scopes = computed(() => { const base = result.value?.serverInfo.scopes || [] const cloned = cloneDeep(base) // cause it might get directly plopped back into the cache by a dev - return cloned.map((scope) => ({ - ...scope, - name: scope.name as unknown as (typeof AllScopes)[number] - })) + return cloned + .map((scope) => ({ + ...scope, + name: scope.name as unknown as (typeof AllScopes)[number] + })) + .sort((a, b) => a.name.localeCompare(b.name)) }) return { From 17c6f71ba5b4ab3f74928bf746bee891c327ebdd Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:35:22 +0000 Subject: [PATCH 07/21] feat(fe2 helm chart): allows nodejs inspect flag to be enabled (#3770) --- .../templates/frontend_2/deployment.yml | 6 ++++++ utils/helm/speckle-server/values.schema.json | 15 +++++++++++++++ utils/helm/speckle-server/values.yaml | 5 +++++ 3 files changed, 26 insertions(+) diff --git a/utils/helm/speckle-server/templates/frontend_2/deployment.yml b/utils/helm/speckle-server/templates/frontend_2/deployment.yml index 00809b8ba..d43fbf095 100644 --- a/utils/helm/speckle-server/templates/frontend_2/deployment.yml +++ b/utils/helm/speckle-server/templates/frontend_2/deployment.yml @@ -22,6 +22,12 @@ spec: - name: main image: {{ default (printf "speckle/speckle-frontend-2:%s" .Values.docker_image_tag) .Values.frontend_2.image }} imagePullPolicy: {{ .Values.imagePullPolicy }} + args: #overwrites the Dockerfile CMD statement + - "/nodejs/bin/node" + {{- if .Values.frontend_2.inspect.enabled }} + - {{ printf "--inspect=%s" .Values.frontend_2.inspect.port }} + {{- end }} + - "./server/index.mjs" ports: - name: www diff --git a/utils/helm/speckle-server/values.schema.json b/utils/helm/speckle-server/values.schema.json index ecba2e8c9..55f5d6934 100644 --- a/utils/helm/speckle-server/values.schema.json +++ b/utils/helm/speckle-server/values.schema.json @@ -1774,6 +1774,21 @@ "description": "If enabled, will output logs in a human-readable format. Otherwise, logs will be output in JSON format.", "default": false }, + "inspect": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "If enabled, indicates that the Speckle FE2 should be deployed with the nodejs inspect feature enabled", + "default": false + }, + "port": { + "type": "string", + "description": "The port on which the nodejs inspect feature should be exposed", + "default": "7000" + } + } + }, "enabled": { "type": "boolean", "description": "Feature flag to enable running the new web application frontend.", diff --git a/utils/helm/speckle-server/values.yaml b/utils/helm/speckle-server/values.yaml index 44a79e99a..8922b5b00 100644 --- a/utils/helm/speckle-server/values.yaml +++ b/utils/helm/speckle-server/values.yaml @@ -1062,6 +1062,11 @@ frontend_2: ## @param frontend_2.logPretty If enabled, will output logs in a human-readable format. Otherwise, logs will be output in JSON format. ## logPretty: false + inspect: + ## @param frontend_2.inspect.enabled If enabled, indicates that the Speckle FE2 should be deployed with the nodejs inspect feature enabled + enabled: false + ## @param frontend_2.inspect.port The port on which the nodejs inspect feature should be exposed + port: '7000' ## @param frontend_2.enabled Feature flag to enable running the new web application frontend. ## enabled: true From da10e35920d984d6b91d92c531f15fe6ec3a3295 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 7 Jan 2025 21:47:57 +0100 Subject: [PATCH 08/21] Fix: Small UI fixes for auth screens (#3775) --- .../components/auth/PasswordResetPanel.vue | 8 +++++--- .../frontend-2/components/auth/RegisterPanel.vue | 15 +++++++-------- .../frontend-2/pages/authn/forgotten-password.vue | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/frontend-2/components/auth/PasswordResetPanel.vue b/packages/frontend-2/components/auth/PasswordResetPanel.vue index 8d8092e70..d72fd678a 100644 --- a/packages/frontend-2/components/auth/PasswordResetPanel.vue +++ b/packages/frontend-2/components/auth/PasswordResetPanel.vue @@ -1,6 +1,8 @@