4586 fix workspace member feature (#4680)

* Fix import

* Handle delete workspace member consequences

* Add a patch to request deleted workspace member's userId

* Remove useless relations

* Handle delete workspace + refactor

* Add missing migration

* Fix test

* Code review returns

* Add missing operation in migration file

* Fix code review return update

* Fix workspaceMember<>ConnectedAccount relation
This commit is contained in:
martmull
2024-03-28 17:59:48 +01:00
committed by GitHub
parent 00eee3158e
commit 27fdb00d07
14 changed files with 298 additions and 127 deletions

View File

@ -113,4 +113,46 @@ export class UserService extends TypeOrmQueryService<User> {
return user;
}
async handleRemoveWorkspaceMember(workspaceId: string, userId: string) {
await this.userWorkspaceRepository.delete({
userId,
workspaceId,
});
await this.reassignOrRemoveUserDefaultWorkspace(workspaceId, userId);
}
private async reassignOrRemoveUserDefaultWorkspace(
workspaceId: string,
userId: string,
) {
const userWorkspaces = await this.userWorkspaceRepository.find({
where: { userId: userId },
});
if (userWorkspaces.length === 0) {
await this.userRepository.delete({ id: userId });
return;
}
const user = await this.userRepository.findOne({
where: {
id: userId,
},
});
if (!user) {
throw new Error(`User ${userId} not found in workspace ${workspaceId}`);
}
if (user.defaultWorkspaceId === workspaceId) {
await this.userRepository.update(
{ id: userId },
{
defaultWorkspaceId: userWorkspaces[0].workspaceId,
},
);
}
}
}

View File

@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
export type HandleWorkspaceMemberDeletedJobData = {
workspaceId: string;
userId: string;
};
@Injectable()
export class HandleWorkspaceMemberDeletedJob
implements MessageQueueJob<HandleWorkspaceMemberDeletedJobData>
{
constructor(private readonly userService: UserService) {}
async handle(data: HandleWorkspaceMemberDeletedJobData): Promise<void> {
const { workspaceId, userId } = data;
await this.userService.handleRemoveWorkspaceMember(workspaceId, userId);
}
}

View File

@ -7,6 +7,7 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works
import { User } from 'src/engine/core-modules/user/user.entity';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { WorkspaceService } from './workspace.service';
@ -37,6 +38,10 @@ describe('WorkspaceService', () => {
provide: UserWorkspaceService,
useValue: {},
},
{
provide: UserService,
useValue: {},
},
{
provide: BillingService,
useValue: {},

View File

@ -13,6 +13,7 @@ import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/a
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
export class WorkspaceService extends TypeOrmQueryService<Workspace> {
constructor(
@ -20,11 +21,10 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(UserWorkspace, 'core')
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
private readonly workspaceManagerService: WorkspaceManagerService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly billingService: BillingService,
private readonly userService: UserService,
) {
super(workspaceRepository);
}
@ -49,7 +49,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
return await this.workspaceManagerService.doesDataSourceExist(id);
}
async deleteWorkspace(id: string, shouldDeleteCoreWorkspace = true) {
async solfDeleteWorkspace(id: string) {
const workspace = await this.workspaceRepository.findOneBy({ id });
assert(workspace, 'Workspace not found');
@ -58,9 +58,24 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
await this.billingService.deleteSubscription(workspace.id);
await this.workspaceManagerService.delete(id);
if (shouldDeleteCoreWorkspace) {
await this.workspaceRepository.delete(id);
return workspace;
}
async deleteWorkspace(id: string) {
const userWorkspaces = await this.userWorkspaceRepository.findBy({
workspaceId: id,
});
const workspace = await this.solfDeleteWorkspace(id);
for (const userWorkspace of userWorkspaces) {
await this.userService.handleRemoveWorkspaceMember(
id,
userWorkspace.userId,
);
}
await this.workspaceRepository.delete(id);
return workspace;
}
@ -70,118 +85,4 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
.find()
.then((workspaces) => workspaces.map((workspace) => workspace.id));
}
private async reassignDefaultWorkspace(
currentWorkspaceId: string,
user: User,
worskpaces: UserWorkspace[],
) {
// We'll filter all user workspaces without the one which its getting removed from
const filteredUserWorkspaces = worskpaces.filter(
(workspace) => workspace.workspaceId !== currentWorkspaceId,
);
// Loop over each workspace in the filteredUserWorkspaces array and check if it currently exists in
// the database
for (let index = 0; index < filteredUserWorkspaces.length; index++) {
const userWorkspace = filteredUserWorkspaces[index];
const nextWorkspace = await this.workspaceRepository.findOneBy({
id: userWorkspace.workspaceId,
});
if (nextWorkspace) {
await this.userRepository.save({
id: user.id,
defaultWorkspace: nextWorkspace,
updatedAt: new Date().toISOString(),
});
break;
}
// if no workspaces are valid then we delete the user
if (index === filteredUserWorkspaces.length - 1) {
await this.userRepository.delete({ id: user.id });
}
}
}
/*
async removeWorkspaceMember(workspaceId: string, memberId: string) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
);
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
// using "SELECT *" here because we will need the corresponding members userId later
const [workspaceMember] = await workspaceDataSource?.query(
`SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "id" = '${memberId}'`,
);
if (!workspaceMember) {
throw new NotFoundException('Member not found.');
}
await workspaceDataSource?.query(
`DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "id" = '${memberId}'`,
);
const workspaceMemberUser = await this.userRepository.findOne({
where: {
id: workspaceMember.userId,
},
relations: ['defaultWorkspace'],
});
if (!workspaceMemberUser) {
throw new NotFoundException('User not found');
}
const userWorkspaces = await this.userWorkspaceRepository.find({
where: { userId: workspaceMemberUser.id },
relations: ['workspace'],
});
// We want to check if we the user has signed up to more than one workspace
if (userWorkspaces.length > 1) {
// We neeed to check if the workspace that its getting removed from is its default workspace, if it is then
// change the default workspace to point to the next workspace available.
if (workspaceMemberUser.defaultWorkspace.id === workspaceId) {
await this.reassignDefaultWorkspace(
workspaceId,
workspaceMemberUser,
userWorkspaces,
);
}
// if its not the default workspace then simply delete the user-workspace mapping
await this.userWorkspaceRepository.delete({
userId: workspaceMemberUser.id,
workspaceId,
});
} else {
await this.userWorkspaceRepository.delete({
userId: workspaceMemberUser.id,
});
// After deleting the user-workspace mapping, we have a condition where we have the users default workspace points to a
// workspace which it doesnt have access to. So we delete the user.
await this.userRepository.delete({ id: workspaceMemberUser.id });
}
const payload =
new ObjectRecordDeleteEvent<WorkspaceMemberObjectMetadata>();
payload.workspaceId = workspaceId;
payload.details = {
before: workspaceMember,
};
this.eventEmitter.emit('workspaceMember.deleted', payload);
return memberId;
}
*/
}

View File

@ -0,0 +1,35 @@
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata';
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
import {
HandleWorkspaceMemberDeletedJob,
HandleWorkspaceMemberDeletedJobData,
} from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job';
@Injectable()
export class WorkspaceWorkspaceMemberListener {
constructor(
@Inject(MessageQueue.workspaceQueue)
private readonly messageQueueService: MessageQueueService,
) {}
@OnEvent('workspaceMember.deleted')
async handleDeleteEvent(
payload: ObjectRecordDeleteEvent<WorkspaceMemberObjectMetadata>,
) {
const userId = payload.details.before.userId;
if (!userId) {
return;
}
await this.messageQueueService.add<HandleWorkspaceMemberDeletedJobData>(
HandleWorkspaceMemberDeletedJob.name,
{ workspaceId: payload.workspaceId, userId },
);
}
}

View File

@ -8,11 +8,12 @@ import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.r
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener';
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
import { Workspace } from './workspace.entity';
@ -27,19 +28,24 @@ import { WorkspaceService } from './services/workspace.service';
BillingModule,
FileUploadModule,
NestjsQueryTypeOrmModule.forFeature(
[User, Workspace, UserWorkspace, FeatureFlagEntity],
[Workspace, UserWorkspace, FeatureFlagEntity],
'core',
),
UserWorkspaceModule,
WorkspaceManagerModule,
DataSourceModule,
TypeORMModule,
UserModule,
],
services: [WorkspaceService],
resolvers: workspaceAutoResolverOpts,
}),
],
exports: [WorkspaceService],
providers: [WorkspaceResolver, WorkspaceService],
providers: [
WorkspaceResolver,
WorkspaceService,
WorkspaceWorkspaceMemberListener,
],
})
export class WorkspaceModule {}