chore: Permanent team deletion (#2493)

This commit is contained in:
Tom Moor
2021-09-20 20:58:39 -07:00
committed by GitHub
parent a88b54d26d
commit e1601fbe72
10 changed files with 425 additions and 24 deletions

View File

@ -4,7 +4,7 @@ import { Document, Attachment } from "../models";
import { sequelize } from "../sequelize";
import parseAttachmentIds from "../utils/parseAttachmentIds";
export async function documentPermanentDeleter(documents: Document[]) {
export default async function documentPermanentDeleter(documents: Document[]) {
const activeDocument = documents.find((doc) => !doc.deletedAt);
if (activeDocument) {

View File

@ -3,7 +3,7 @@ import { subDays } from "date-fns";
import { Attachment, Document } from "../models";
import { buildAttachment, buildDocument } from "../test/factories";
import { flushdb } from "../test/support";
import { documentPermanentDeleter } from "./documentPermanentDeleter";
import documentPermanentDeleter from "./documentPermanentDeleter";
jest.mock("aws-sdk", () => {
const mS3 = { deleteObject: jest.fn().mockReturnThis(), promise: jest.fn() };

View File

@ -0,0 +1,177 @@
// @flow
import Logger from "../logging/logger";
import {
ApiKey,
Attachment,
AuthenticationProvider,
Collection,
Document,
Event,
FileOperation,
Group,
Team,
NotificationSetting,
User,
UserAuthentication,
Integration,
SearchQuery,
Share,
} from "../models";
import { sequelize } from "../sequelize";
export default async function teamPermanentDeleter(team: Team) {
if (!team.deletedAt) {
throw new Error(
`Cannot permanently delete ${team.id} team. Please delete it and try again.`
);
}
Logger.info(
"commands",
`Permanently deleting team ${team.name} (${team.id})`
);
const teamId = team.id;
let transaction;
try {
transaction = await sequelize.transaction();
await Attachment.findAllInBatches(
{
where: {
teamId,
},
limit: 100,
offset: 0,
},
async (attachments, options) => {
Logger.info(
"commands",
`Deleting attachments ${options.offset} ${
options.offset + options.limit
}`
);
await Promise.all(
attachments.map((attachment) => attachment.destroy({ transaction }))
);
}
);
// Destroy user-relation models
await User.findAllInBatches(
{
attributes: ["id"],
where: {
teamId,
},
limit: 100,
offset: 0,
},
async (users) => {
const userIds = users.map((user) => user.id);
await UserAuthentication.destroy({
where: { userId: userIds },
force: true,
transaction,
});
await ApiKey.destroy({
where: { userId: userIds },
force: true,
transaction,
});
}
);
// Destory team-relation models
await AuthenticationProvider.destroy({
where: { teamId },
force: true,
transaction,
});
// events must be first due to db constraints
await Event.destroy({
where: { teamId },
force: true,
transaction,
});
await Collection.destroy({
where: { teamId },
force: true,
transaction,
});
await Document.unscoped().destroy({
where: { teamId },
force: true,
transaction,
});
await FileOperation.destroy({
where: { teamId },
force: true,
transaction,
});
await Group.unscoped().destroy({
where: { teamId },
force: true,
transaction,
});
await Integration.destroy({
where: { teamId },
force: true,
transaction,
});
await NotificationSetting.destroy({
where: { teamId },
force: true,
transaction,
});
await SearchQuery.destroy({
where: { teamId },
force: true,
transaction,
});
await Share.destroy({
where: { teamId },
force: true,
transaction,
});
await User.destroy({
where: { teamId },
force: true,
transaction,
});
await team.destroy({
force: true,
transaction,
});
await Event.create(
{
name: "teams.destroy",
modelId: teamId,
},
{ transaction }
);
await transaction.commit();
} catch (err) {
if (transaction) {
await transaction.rollback();
}
throw err;
}
}

View File

@ -0,0 +1,100 @@
// @flow
import { subDays } from "date-fns";
import { Attachment, User, Document, Collection, Team } from "../models";
import {
buildAttachment,
buildUser,
buildTeam,
buildDocument,
} from "../test/factories";
import { flushdb } from "../test/support";
import teamPermanentDeleter from "./teamPermanentDeleter";
jest.mock("aws-sdk", () => {
const mS3 = { deleteObject: jest.fn().mockReturnThis(), promise: jest.fn() };
return {
S3: jest.fn(() => mS3),
Endpoint: jest.fn(),
};
});
beforeEach(() => flushdb());
describe("teamPermanentDeleter", () => {
it("should destroy related data", async () => {
const team = await buildTeam({
deletedAt: subDays(new Date(), 90),
});
const user = await buildUser({
teamId: team.id,
});
await buildDocument({
teamId: team.id,
userId: user.id,
});
await teamPermanentDeleter(team);
expect(await Team.count()).toEqual(0);
expect(await User.count()).toEqual(0);
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
expect(await Collection.unscoped().count({ paranoid: false })).toEqual(0);
});
it("should not destroy unrelated data", async () => {
const team = await buildTeam({
deletedAt: subDays(new Date(), 90),
});
await buildUser();
await buildTeam();
await buildDocument();
await teamPermanentDeleter(team);
expect(await Team.count()).toEqual(4); // each build command creates a team
expect(await User.count()).toEqual(2);
expect(await Document.unscoped().count({ paranoid: false })).toEqual(1);
expect(await Collection.unscoped().count({ paranoid: false })).toEqual(1);
});
it("should destroy attachments", async () => {
const team = await buildTeam({
deletedAt: subDays(new Date(), 90),
});
const user = await buildUser({
teamId: team.id,
});
const document = await buildDocument({
teamId: team.id,
userId: user.id,
});
await buildAttachment({
teamId: document.teamId,
documentId: document.id,
});
await teamPermanentDeleter(team);
expect(await Team.count()).toEqual(0);
expect(await User.count()).toEqual(0);
expect(await Attachment.count()).toEqual(0);
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
expect(await Collection.unscoped().count({ paranoid: false })).toEqual(0);
});
it("should error when trying to destroy undeleted team", async () => {
const team = await buildTeam();
let error;
try {
await teamPermanentDeleter(team);
} catch (err) {
error = err.message;
}
expect(error).toEqual(
`Cannot permanently delete ${team.id} team. Please delete it and try again.`
);
});
});