Skip to content

Commit

Permalink
Merge pull request #276 from tnc-ca-geo/feature/227-bulk_image_deletion
Browse files Browse the repository at this point in the history
Bulk Image Deletion Task
  • Loading branch information
jue-henry authored Nov 13, 2024
2 parents 72b7e99 + ee5a9b4 commit ac4d210
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 1 deletion.
20 changes: 20 additions & 0 deletions src/@types/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,14 @@ export type DeleteImageCommentInput = {
imageId: Scalars['ID']['input'];
};

export type DeleteImagesByFilterInput = {
filters: FiltersInput;
};

export type DeleteImagesByFilterTaskInput = {
filters: FiltersInput;
};

export type DeleteImagesInput = {
imageIds?: InputMaybe<Array<Scalars['ID']['input']>>;
};
Expand Down Expand Up @@ -570,6 +578,8 @@ export type Mutation = {
deleteDeployment?: Maybe<Task>;
deleteImageComment?: Maybe<ImageCommentsPayload>;
deleteImages?: Maybe<StandardErrorPayload>;
deleteImagesByFilterTask?: Maybe<Task>;
deleteImagesTask?: Maybe<Task>;
deleteLabels?: Maybe<StandardPayload>;
deleteObjects?: Maybe<StandardPayload>;
deleteProjectLabel?: Maybe<StandardPayload>;
Expand Down Expand Up @@ -687,6 +697,16 @@ export type MutationDeleteImagesArgs = {
};


export type MutationDeleteImagesByFilterTaskArgs = {
input: DeleteImagesByFilterTaskInput;
};


export type MutationDeleteImagesTaskArgs = {
input: DeleteImagesInput;
};


export type MutationDeleteLabelsArgs = {
input: DeleteLabelsInput;
};
Expand Down
57 changes: 57 additions & 0 deletions src/api/db/models/Image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,53 @@ export class ImageModel {
}
}

/**
* Used by the frontend when the user manually selects and deletes more than
* 100 images at once
*/
static async deleteImagesTask(
input: gql.DeleteImagesInput,
context: Pick<Context, 'config' | 'user'>,
): Promise<HydratedDocument<TaskSchema>> {
try {
return TaskModel.create(
{
type: 'DeleteImages',
projectId: context.user['curr_project'],
user: context.user['cognito:username'],
config: input,
},
context,
);
} catch (err) {
if (err instanceof GraphQLError) throw err;
throw new InternalServerError(err as string);
}
}

/**
* Used by the frontend to delete all currently filtered images
*/
static async deleteImagesByFilterTask(
input: gql.DeleteImagesByFilterTaskInput,
context: Pick<Context, 'config' | 'user'>,
): Promise<HydratedDocument<TaskSchema>> {
try {
return TaskModel.create(
{
type: 'DeleteImagesByFilter',
projectId: context.user['curr_project'],
user: context.user['cognito:username'],
config: input,
},
context,
);
} catch (err) {
if (err instanceof GraphQLError) throw err;
throw new InternalServerError(err as string);
}
}

/**
* A custom middleware-like method that is used to update the reviewed status of
* images that should only be ran by operations that would affect the reviewed status.
Expand Down Expand Up @@ -1138,6 +1185,16 @@ export default class AuthedImageModel extends BaseAuthedModel {
return ImageModel.deleteImages(...args);
}

@roleCheck(DELETE_IMAGES_ROLES)
deleteImagesTask(...args: MethodParams<typeof ImageModel.deleteImagesTask>) {
return ImageModel.deleteImagesTask(...args);
}

@roleCheck(DELETE_IMAGES_ROLES)
deleteImagesByFilterTask(...args: MethodParams<typeof ImageModel.deleteImagesByFilterTask>) {
return ImageModel.deleteImagesByFilterTask(...args);
}

@roleCheck(WRITE_IMAGES_ROLES)
createImage(...args: MethodParams<typeof ImageModel.createImage>) {
return ImageModel.createImage(...args);
Expand Down
2 changes: 2 additions & 0 deletions src/api/db/schemas/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const TaskSchema = new Schema({
'UpdateDeployment',
'DeleteDeployment',
'UpdateSerialNumber',
'DeleteImages',
'DeleteImagesByFilter',
],
},
status: {
Expand Down
16 changes: 16 additions & 0 deletions src/api/resolvers/Mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ export default {
return context.models.Image.deleteImages(input, context);
},

deleteImagesTask: async (
_: unknown,
{ input }: gql.MutationDeleteImagesArgs,
context: Context,
): Promise<gql.Task> => {
return context.models.Image.deleteImagesTask(input, context);
},

deleteImagesByFilterTask: async (
_: unknown,
{ input }: gql.MutationDeleteImagesByFilterTaskArgs,
context: Context,
): Promise<gql.Task> => {
return context.models.Image.deleteImagesByFilterTask(input, context);
},

registerCamera: async (
_: unknown,
{ input }: gql.MutationRegisterCameraArgs,
Expand Down
5 changes: 5 additions & 0 deletions src/api/type-defs/inputs/DeleteImagesByFilterInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default /* GraphQL */ `
input DeleteImagesByFilterInput {
filters: FiltersInput!
}
`;
5 changes: 5 additions & 0 deletions src/api/type-defs/inputs/DeleteImagesByFilterTaskInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default /* GraphQL */ `
input DeleteImagesByFilterTaskInput {
filters: FiltersInput!
}
`;
2 changes: 2 additions & 0 deletions src/api/type-defs/root/Mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export default /* GraphQL */ `
type Mutation {
createImage(input: CreateImageInput!): CreateImagePayload
deleteImages(input: DeleteImagesInput!): StandardErrorPayload
deleteImagesTask(input: DeleteImagesInput!): Task
deleteImagesByFilterTask(input: DeleteImagesByFilterTaskInput!): Task
createImageComment(input: CreateImageCommentInput!): ImageCommentsPayload
updateImageComment(input: UpdateImageCommentInput!): ImageCommentsPayload
Expand Down
5 changes: 5 additions & 0 deletions src/task/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { parseMessage } from './utils.js';
import { type TaskInput } from '../api/db/models/Task.js';
import GraphQLError, { InternalServerError } from '../api/errors.js';
import { type User } from '../api/auth/authorization.js';
import { DeleteImages, DeleteImagesByFilter } from './image.js';

async function handler(event: SQSEvent) {
if (!event.Records || !event.Records.length) return;
Expand Down Expand Up @@ -42,6 +43,10 @@ async function handler(event: SQSEvent) {
output = await DeleteDeployment(task);
} else if (task.type === 'UpdateSerialNumber') {
output = await UpdateSerialNumber(task);
} else if (task.type === 'DeleteImages') {
output = await DeleteImages(task);
} else if (task.type === 'DeleteImagesByFilter') {
output = await DeleteImagesByFilter(task);
} else {
throw new Error(`Unknown Task: ${JSON.stringify(task)}`);
}
Expand Down
48 changes: 48 additions & 0 deletions src/task/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { type User } from '../api/auth/authorization.js';
import { ImageModel } from '../api/db/models/Image.js';
import { TaskInput } from '../api/db/models/Task.js';
import type * as gql from '../@types/graphql.js';

export async function DeleteImagesByFilter(task: TaskInput<gql.DeleteImagesByFilterTaskInput>) {
/**
* Deletes images that match the inputted filters in batches of 100.
* This is used by the frontend to delete all images currently shown.
* * @param {Object} input
* * @param {gql.FiltersInput} input.config.filters
*/
const context = { user: { is_superuser: true, curr_project: task.projectId } as User };
let images = await ImageModel.queryByFilter(
{ filters: task.config.filters, limit: 100 },
context,
);
while (images.results.length > 0) {
const batch = images.results.map((image) => image._id);
await ImageModel.deleteImages({ imageIds: batch }, context);
if (images.hasNext) {
images = await ImageModel.queryByFilter(
{ filters: task.config.filters, limit: 100, next: images.next },
context,
);
} else {
break;
}
}

return { filters: task.config.filters };
}

export async function DeleteImages(task: TaskInput<gql.DeleteImagesInput>) {
/**
* Deletes a list of images by their IDs in batches of 100.
* This is used by the frontend when the user is selecting more than 100 images to delete to delete at once.
* * @param {Object} input
* * @param {String[]} input.config.imageIds
*/
const context = { user: { is_superuser: true, curr_project: task.projectId } as User };
const imagesToDelete = task.config.imageIds?.slice() ?? [];
while (imagesToDelete.length > 0) {
const batch = imagesToDelete.splice(0, 100);
await ImageModel.deleteImages({ imageIds: batch }, context);
}
return { imageIds: task.config.imageIds };
}
2 changes: 1 addition & 1 deletion src/task/stats.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ProjectModel } from '../api/db/models/Project.js';
import { buildPipeline, isImageReviewed, idMatch } from '../api/db/models/utils.js';
import { buildPipeline, idMatch } from '../api/db/models/utils.js';
import Image, { type ImageSchema } from '../api/db/schemas/Image.js';
import _ from 'lodash';
import { type TaskInput } from '../api/db/models/Task.js';
Expand Down

0 comments on commit ac4d210

Please sign in to comment.