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:
@ -0,0 +1,102 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddMissingMigration1711557405330 implements MigrationInterface {
|
||||||
|
name = 'AddMissingMigration1711557405330';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_37fdc7357af701e595c5c3a9bd6"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_cb488f32c6a0827b938edadf221"`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE USING "createdAt"::TIMESTAMP WITH TIME ZONE`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ALTER COLUMN "createdAt" SET DEFAULT now()`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ALTER COLUMN "createdAt" SET NOT NULL;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE USING "updatedAt"::TIMESTAMP WITH TIME ZONE`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ALTER COLUMN "updatedAt" SET DEFAULT now()`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ALTER COLUMN "updatedAt" SET NOT NULL;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ALTER COLUMN "deletedAt" TYPE TIMESTAMP WITH TIME ZONE USING "deletedAt"::TIMESTAMP WITH TIME ZONE`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."user" DROP CONSTRAINT "FK_2ec910029395fa7655621c88908"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."user" ALTER COLUMN "defaultWorkspaceId" SET NOT NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ADD CONSTRAINT "IndexOnUserIdAndWorkspaceIdUnique" UNIQUE ("userId", "workspaceId")`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ADD CONSTRAINT "FK_a2da2ea7d6cd1e5a4c5cb1791f8" FOREIGN KEY ("userId") REFERENCES "core"."user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ADD CONSTRAINT "FK_22f5e76f493c3fb20237cfc48b0" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."user" ADD CONSTRAINT "FK_2ec910029395fa7655621c88908" FOREIGN KEY ("defaultWorkspaceId") REFERENCES "core"."workspace"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."user" DROP CONSTRAINT "FK_2ec910029395fa7655621c88908"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_22f5e76f493c3fb20237cfc48b0"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_a2da2ea7d6cd1e5a4c5cb1791f8"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "IndexOnUserIdAndWorkspaceIdUnique"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."user" ALTER COLUMN "defaultWorkspaceId" DROP NOT NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."user" ADD CONSTRAINT "FK_2ec910029395fa7655621c88908" FOREIGN KEY ("defaultWorkspaceId") REFERENCES "core"."workspace"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" DROP COLUMN "deletedAt"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ADD "deletedAt" TIMESTAMP`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" DROP COLUMN "updatedAt"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ADD "updatedAt" TIMESTAMP NOT NULL DEFAULT now()`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" DROP COLUMN "createdAt"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT now()`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ADD CONSTRAINT "FK_cb488f32c6a0827b938edadf221" FOREIGN KEY ("userId") REFERENCES "core"."user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ADD CONSTRAINT "FK_37fdc7357af701e595c5c3a9bd6" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,9 +19,21 @@ export class UpdateRefreshTokenTable1711624086253
|
|||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`ALTER TABLE "core"."appToken" ADD CONSTRAINT "FK_d6ae19a7aa2bbd4919053257772" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
`ALTER TABLE "core"."appToken" ADD CONSTRAINT "FK_d6ae19a7aa2bbd4919053257772" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
);
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."appToken" DROP CONSTRAINT "FK_7008a2b0fb083127f60b5f4448e"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."appToken" ADD CONSTRAINT "FK_8cd4819144baf069777b5729136" FOREIGN KEY ("userId") REFERENCES "core"."user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."appToken" ADD CONSTRAINT "FK_7008a2b0fb083127f60b5f4448e" FOREIGN KEY ("userId") REFERENCES "core"."user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."appToken" DROP CONSTRAINT "FK_8cd4819144baf069777b5729136"`,
|
||||||
|
);
|
||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`ALTER TABLE "core"."appToken" DROP CONSTRAINT "FK_d6ae19a7aa2bbd4919053257772"`,
|
`ALTER TABLE "core"."appToken" DROP CONSTRAINT "FK_d6ae19a7aa2bbd4919053257772"`,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -407,6 +407,15 @@ export class WorkspaceQueryRunnerService {
|
|||||||
args,
|
args,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO START: remove this awful patch and use our upcoming custom ORM is developed
|
||||||
|
const deletedWorkspaceMember = await this.handleDeleteWorkspaceMember(
|
||||||
|
args.id,
|
||||||
|
workspaceId,
|
||||||
|
objectMetadataItem,
|
||||||
|
);
|
||||||
|
// TODO END
|
||||||
|
|
||||||
const result = await this.execute(query, workspaceId);
|
const result = await this.execute(query, workspaceId);
|
||||||
|
|
||||||
const parsedResults = (
|
const parsedResults = (
|
||||||
@ -429,7 +438,10 @@ export class WorkspaceQueryRunnerService {
|
|||||||
recordId: args.id,
|
recordId: args.id,
|
||||||
objectMetadata: objectMetadataItem,
|
objectMetadata: objectMetadataItem,
|
||||||
details: {
|
details: {
|
||||||
before: this.removeNestedProperties(parsedResults?.[0]),
|
before: {
|
||||||
|
...(deletedWorkspaceMember ?? {}),
|
||||||
|
...this.removeNestedProperties(parsedResults?.[0]),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} satisfies ObjectRecordDeleteEvent<any>);
|
} satisfies ObjectRecordDeleteEvent<any>);
|
||||||
|
|
||||||
@ -555,4 +567,33 @@ export class WorkspaceQueryRunnerService {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleDeleteWorkspaceMember(
|
||||||
|
id: string,
|
||||||
|
workspaceId: string,
|
||||||
|
objectMetadataItem: ObjectMetadataInterface,
|
||||||
|
) {
|
||||||
|
if (objectMetadataItem.nameSingular !== 'workspaceMember') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceMemberResult = await this.executeAndParse<IRecord>(
|
||||||
|
`
|
||||||
|
query {
|
||||||
|
workspaceMemberCollection(filter: {id: {eq: "${id}"}}) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
userId: userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
objectMetadataItem,
|
||||||
|
'',
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return workspaceMemberResult.edges?.[0]?.node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,4 +113,46 @@ export class UserService extends TypeOrmQueryService<User> {
|
|||||||
|
|
||||||
return 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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
|
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
|
||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.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';
|
import { WorkspaceService } from './workspace.service';
|
||||||
|
|
||||||
@ -37,6 +38,10 @@ describe('WorkspaceService', () => {
|
|||||||
provide: UserWorkspaceService,
|
provide: UserWorkspaceService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: UserService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: BillingService,
|
provide: BillingService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
|
|||||||
@ -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 { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
import { BillingService } from 'src/engine/core-modules/billing/billing.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> {
|
export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||||
constructor(
|
constructor(
|
||||||
@ -20,11 +21,10 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
private readonly workspaceRepository: Repository<Workspace>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
@InjectRepository(UserWorkspace, 'core')
|
@InjectRepository(UserWorkspace, 'core')
|
||||||
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||||
@InjectRepository(User, 'core')
|
|
||||||
private readonly userRepository: Repository<User>,
|
|
||||||
private readonly workspaceManagerService: WorkspaceManagerService,
|
private readonly workspaceManagerService: WorkspaceManagerService,
|
||||||
private readonly userWorkspaceService: UserWorkspaceService,
|
private readonly userWorkspaceService: UserWorkspaceService,
|
||||||
private readonly billingService: BillingService,
|
private readonly billingService: BillingService,
|
||||||
|
private readonly userService: UserService,
|
||||||
) {
|
) {
|
||||||
super(workspaceRepository);
|
super(workspaceRepository);
|
||||||
}
|
}
|
||||||
@ -49,7 +49,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
return await this.workspaceManagerService.doesDataSourceExist(id);
|
return await this.workspaceManagerService.doesDataSourceExist(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteWorkspace(id: string, shouldDeleteCoreWorkspace = true) {
|
async solfDeleteWorkspace(id: string) {
|
||||||
const workspace = await this.workspaceRepository.findOneBy({ id });
|
const workspace = await this.workspaceRepository.findOneBy({ id });
|
||||||
|
|
||||||
assert(workspace, 'Workspace not found');
|
assert(workspace, 'Workspace not found');
|
||||||
@ -58,9 +58,24 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
await this.billingService.deleteSubscription(workspace.id);
|
await this.billingService.deleteSubscription(workspace.id);
|
||||||
|
|
||||||
await this.workspaceManagerService.delete(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;
|
return workspace;
|
||||||
}
|
}
|
||||||
@ -70,118 +85,4 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
.find()
|
.find()
|
||||||
.then((workspaces) => workspaces.map((workspace) => workspace.id));
|
.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;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,11 +8,12 @@ import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.r
|
|||||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
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 { 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 { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.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 { 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 { 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 { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
|
||||||
import { Workspace } from './workspace.entity';
|
import { Workspace } from './workspace.entity';
|
||||||
@ -27,19 +28,24 @@ import { WorkspaceService } from './services/workspace.service';
|
|||||||
BillingModule,
|
BillingModule,
|
||||||
FileUploadModule,
|
FileUploadModule,
|
||||||
NestjsQueryTypeOrmModule.forFeature(
|
NestjsQueryTypeOrmModule.forFeature(
|
||||||
[User, Workspace, UserWorkspace, FeatureFlagEntity],
|
[Workspace, UserWorkspace, FeatureFlagEntity],
|
||||||
'core',
|
'core',
|
||||||
),
|
),
|
||||||
UserWorkspaceModule,
|
UserWorkspaceModule,
|
||||||
WorkspaceManagerModule,
|
WorkspaceManagerModule,
|
||||||
DataSourceModule,
|
DataSourceModule,
|
||||||
TypeORMModule,
|
TypeORMModule,
|
||||||
|
UserModule,
|
||||||
],
|
],
|
||||||
services: [WorkspaceService],
|
services: [WorkspaceService],
|
||||||
resolvers: workspaceAutoResolverOpts,
|
resolvers: workspaceAutoResolverOpts,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
exports: [WorkspaceService],
|
exports: [WorkspaceService],
|
||||||
providers: [WorkspaceResolver, WorkspaceService],
|
providers: [
|
||||||
|
WorkspaceResolver,
|
||||||
|
WorkspaceService,
|
||||||
|
WorkspaceWorkspaceMemberListener,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class WorkspaceModule {}
|
export class WorkspaceModule {}
|
||||||
|
|||||||
@ -47,6 +47,7 @@ import { MessageChannelObjectMetadata } from 'src/modules/messaging/standard-obj
|
|||||||
import { SaveEventToDbJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job';
|
import { SaveEventToDbJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job';
|
||||||
import { CreateCompanyAndContactJob } from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job';
|
import { CreateCompanyAndContactJob } from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job';
|
||||||
import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata';
|
import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata';
|
||||||
|
import { HandleWorkspaceMemberDeletedJob } from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job';
|
||||||
import { GmailFullSynV2Module } from 'src/modules/messaging/services/gmail-full-sync-v2/gmail-full-sync.v2.module';
|
import { GmailFullSynV2Module } from 'src/modules/messaging/services/gmail-full-sync-v2/gmail-full-sync.v2.module';
|
||||||
import { GmailFetchMessageContentFromCacheModule } from 'src/modules/messaging/services/gmail-fetch-message-content-from-cache/gmail-fetch-message-content-from-cache.module';
|
import { GmailFetchMessageContentFromCacheModule } from 'src/modules/messaging/services/gmail-fetch-message-content-from-cache/gmail-fetch-message-content-from-cache.module';
|
||||||
import { FetchAllMessagesFromCacheCronJob } from 'src/modules/messaging/commands/crons/fetch-all-messages-from-cache.cron-job';
|
import { FetchAllMessagesFromCacheCronJob } from 'src/modules/messaging/commands/crons/fetch-all-messages-from-cache.cron-job';
|
||||||
@ -139,6 +140,10 @@ import { GmailPartialSyncV2Module } from 'src/modules/messaging/services/gmail-p
|
|||||||
useClass: DeleteConnectedAccountAssociatedCalendarDataJob,
|
useClass: DeleteConnectedAccountAssociatedCalendarDataJob,
|
||||||
},
|
},
|
||||||
{ provide: UpdateSubscriptionJob.name, useClass: UpdateSubscriptionJob },
|
{ provide: UpdateSubscriptionJob.name, useClass: UpdateSubscriptionJob },
|
||||||
|
{
|
||||||
|
provide: HandleWorkspaceMemberDeletedJob.name,
|
||||||
|
useClass: HandleWorkspaceMemberDeletedJob,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: RecordPositionBackfillJob.name,
|
provide: RecordPositionBackfillJob.name,
|
||||||
useClass: RecordPositionBackfillJob,
|
useClass: RecordPositionBackfillJob,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export enum MessageQueue {
|
|||||||
calendarQueue = 'calendar-queue',
|
calendarQueue = 'calendar-queue',
|
||||||
contactCreationQueue = 'contact-creation-queue',
|
contactCreationQueue = 'contact-creation-queue',
|
||||||
billingQueue = 'billing-queue',
|
billingQueue = 'billing-queue',
|
||||||
|
workspaceQueue = 'workspace-queue',
|
||||||
recordPositionBackfillQueue = 'record-position-backfill-queue',
|
recordPositionBackfillQueue = 'record-position-backfill-queue',
|
||||||
entityEventsToDbQueue = 'entity-events-to-db-queue',
|
entityEventsToDbQueue = 'entity-events-to-db-queue',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,10 +83,7 @@ export class DeleteIncompleteWorkspacesCommand extends CommandRunner {
|
|||||||
} name: '${incompleteWorkspace.displayName}'`,
|
} name: '${incompleteWorkspace.displayName}'`,
|
||||||
);
|
);
|
||||||
if (!options.dryRun) {
|
if (!options.dryRun) {
|
||||||
await this.workspaceService.deleteWorkspace(
|
await this.workspaceService.solfDeleteWorkspace(incompleteWorkspace.id);
|
||||||
incompleteWorkspace.id,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -186,6 +186,7 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
|
|||||||
type: RelationMetadataType.ONE_TO_MANY,
|
type: RelationMetadataType.ONE_TO_MANY,
|
||||||
inverseSideTarget: () => ConnectedAccountObjectMetadata,
|
inverseSideTarget: () => ConnectedAccountObjectMetadata,
|
||||||
inverseSideFieldKey: 'accountOwner',
|
inverseSideFieldKey: 'accountOwner',
|
||||||
|
onDelete: RelationOnDeleteAction.CASCADE,
|
||||||
})
|
})
|
||||||
connectedAccounts: ConnectedAccountObjectMetadata[];
|
connectedAccounts: ConnectedAccountObjectMetadata[];
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
"types": ["jest", "node"],
|
"types": ["jest", "node"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"src/*": ["packages/twenty-server/src/*"],
|
"src/*": ["packages/twenty-server/src/*"],
|
||||||
|
"test/*": ["packages/twenty-server/test/*"],
|
||||||
"twenty-emails": ["packages/twenty-emails/src/index.ts"]
|
"twenty-emails": ["packages/twenty-emails/src/index.ts"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user