Skip to content

Commit

Permalink
Merge pull request #277 from tnc-ca-geo/feature/138-image-tags
Browse files Browse the repository at this point in the history
138 Image level tags
  • Loading branch information
nathanielrindlaub authored Dec 3, 2024
2 parents ac4d210 + 21d51c7 commit 6353c55
Show file tree
Hide file tree
Showing 20 changed files with 408 additions and 3 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 74 additions & 0 deletions src/@types/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ export type CreateImagePayload = {
imageAttempt?: Maybe<ImageAttempt>;
};

export type CreateImageTagInput = {
imageId: Scalars['ID']['input'];
tagId: Scalars['ID']['input'];
};

export type CreateInternalLabelInput = {
bbox: Array<Scalars['Float']['input']>;
conf?: InputMaybe<Scalars['Float']['input']>;
Expand Down Expand Up @@ -215,6 +220,11 @@ export type CreateProjectLabelInput = {
reviewerEnabled?: InputMaybe<Scalars['Boolean']['input']>;
};

export type CreateProjectTagInput = {
color: Scalars['String']['input'];
name: Scalars['String']['input'];
};

export type CreateUploadInput = {
originalFile: Scalars['String']['input'];
partCount?: InputMaybe<Scalars['Int']['input']>;
Expand Down Expand Up @@ -256,6 +266,11 @@ export type DeleteImageCommentInput = {
imageId: Scalars['ID']['input'];
};

export type DeleteImageTagInput = {
imageId: Scalars['ID']['input'];
tagId: Scalars['ID']['input'];
};

export type DeleteImagesByFilterInput = {
filters: FiltersInput;
};
Expand Down Expand Up @@ -291,6 +306,10 @@ export type DeleteProjectLabelInput = {
_id: Scalars['ID']['input'];
};

export type DeleteProjectTagInput = {
_id: Scalars['ID']['input'];
};

export type DeleteViewInput = {
viewId: Scalars['ID']['input'];
};
Expand Down Expand Up @@ -424,6 +443,7 @@ export type Image = {
path?: Maybe<Scalars['String']['output']>;
projectId: Scalars['String']['output'];
reviewed?: Maybe<Scalars['Boolean']['output']>;
tags?: Maybe<Array<Scalars['ID']['output']>>;
timezone: Scalars['String']['output'];
userSetData?: Maybe<Scalars['JSONObject']['output']>;
};
Expand Down Expand Up @@ -491,6 +511,11 @@ export type ImageMetadata = {
timezone?: Maybe<Scalars['String']['output']>;
};

export type ImageTagsPayload = {
__typename?: 'ImageTagsPayload';
tags?: Maybe<Array<Scalars['ID']['output']>>;
};

export type ImagesConnection = {
__typename?: 'ImagesConnection';
images: Array<Image>;
Expand Down Expand Up @@ -567,22 +592,26 @@ export type Mutation = {
createImage?: Maybe<CreateImagePayload>;
createImageComment?: Maybe<ImageCommentsPayload>;
createImageError?: Maybe<ImageError>;
createImageTag?: Maybe<ImageTagsPayload>;
createInternalLabels?: Maybe<StandardPayload>;
createLabels?: Maybe<StandardPayload>;
createObjects?: Maybe<StandardPayload>;
createProject?: Maybe<ProjectPayload>;
createProjectLabel?: Maybe<ProjectLabelPayload>;
createProjectTag?: Maybe<ProjectTagsPayload>;
createUpload?: Maybe<CreateUploadPayload>;
createUser?: Maybe<StandardPayload>;
createView?: Maybe<CreateViewPayload>;
deleteDeployment?: Maybe<Task>;
deleteImageComment?: Maybe<ImageCommentsPayload>;
deleteImageTag?: Maybe<ImageTagsPayload>;
deleteImages?: Maybe<StandardErrorPayload>;
deleteImagesByFilterTask?: Maybe<Task>;
deleteImagesTask?: Maybe<Task>;
deleteLabels?: Maybe<StandardPayload>;
deleteObjects?: Maybe<StandardPayload>;
deleteProjectLabel?: Maybe<StandardPayload>;
deleteProjectTag?: Maybe<ProjectTagsPayload>;
deleteView?: Maybe<DeleteViewPayload>;
redriveBatch?: Maybe<StandardPayload>;
registerCamera?: Maybe<RegisterCameraPayload>;
Expand All @@ -597,6 +626,7 @@ export type Mutation = {
updateObjects?: Maybe<StandardPayload>;
updateProject?: Maybe<ProjectPayload>;
updateProjectLabel?: Maybe<ProjectLabelPayload>;
updateProjectTag?: Maybe<ProjectTagsPayload>;
updateUser?: Maybe<StandardPayload>;
updateView?: Maybe<UpdateViewPayload>;
};
Expand Down Expand Up @@ -642,6 +672,11 @@ export type MutationCreateImageErrorArgs = {
};


export type MutationCreateImageTagArgs = {
input: CreateImageTagInput;
};


export type MutationCreateInternalLabelsArgs = {
input: CreateInternalLabelsInput;
};
Expand All @@ -667,6 +702,11 @@ export type MutationCreateProjectLabelArgs = {
};


export type MutationCreateProjectTagArgs = {
input: CreateProjectTagInput;
};


export type MutationCreateUploadArgs = {
input: CreateUploadInput;
};
Expand All @@ -692,6 +732,11 @@ export type MutationDeleteImageCommentArgs = {
};


export type MutationDeleteImageTagArgs = {
input: DeleteImageTagInput;
};


export type MutationDeleteImagesArgs = {
input: DeleteImagesInput;
};
Expand Down Expand Up @@ -722,6 +767,11 @@ export type MutationDeleteProjectLabelArgs = {
};


export type MutationDeleteProjectTagArgs = {
input: DeleteProjectTagInput;
};


export type MutationDeleteViewArgs = {
input: DeleteViewInput;
};
Expand Down Expand Up @@ -792,6 +842,11 @@ export type MutationUpdateProjectLabelArgs = {
};


export type MutationUpdateProjectTagArgs = {
input: UpdateProjectTagInput;
};


export type MutationUpdateUserArgs = {
input: UpdateUserInput;
};
Expand Down Expand Up @@ -864,6 +919,7 @@ export type Project = {
description?: Maybe<Scalars['String']['output']>;
labels?: Maybe<Array<ProjectLabel>>;
name: Scalars['String']['output'];
tags?: Maybe<Array<ProjectTag>>;
timezone: Scalars['String']['output'];
views: Array<View>;
};
Expand Down Expand Up @@ -894,6 +950,18 @@ export type ProjectRegistration = {
projectId: Scalars['String']['output'];
};

export type ProjectTag = {
__typename?: 'ProjectTag';
_id: Scalars['ID']['output'];
color: Scalars['String']['output'];
name: Scalars['String']['output'];
};

export type ProjectTagsPayload = {
__typename?: 'ProjectTagsPayload';
tags?: Maybe<Array<ProjectTag>>;
};

export type Query = {
__typename?: 'Query';
batches?: Maybe<BatchesConnection>;
Expand Down Expand Up @@ -1167,6 +1235,12 @@ export type UpdateProjectLabelInput = {
reviewerEnabled?: InputMaybe<Scalars['Boolean']['input']>;
};

export type UpdateProjectTagInput = {
_id: Scalars['ID']['input'];
color: Scalars['String']['input'];
name: Scalars['String']['input'];
};

export type UpdateUserInput = {
roles: Array<UserRole>;
username: Scalars['String']['input'];
Expand Down
2 changes: 2 additions & 0 deletions src/api/auth/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const WRITE_DEPLOYMENTS_ROLES = [MANAGER];
const WRITE_AUTOMATION_RULES_ROLES = [MANAGER];
const WRITE_CAMERA_REGISTRATION_ROLES = [MANAGER];
const WRITE_CAMERA_SERIAL_NUMBER_ROLES = [MANAGER];
const WRITE_TAGS_ROLES = [MANAGER, MEMBER];

export {
READ_TASKS_ROLES,
Expand All @@ -30,4 +31,5 @@ export {
WRITE_AUTOMATION_RULES_ROLES,
WRITE_CAMERA_REGISTRATION_ROLES,
WRITE_CAMERA_SERIAL_NUMBER_ROLES,
WRITE_TAGS_ROLES,
};
93 changes: 92 additions & 1 deletion src/api/db/models/Image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import GraphQLError, {
NotFoundError,
} from '../../errors.js';
import { BulkWriteResult } from 'mongodb';
import mongoose, { HydratedDocument } from 'mongoose';
import mongoose, { HydratedDocument, UpdateWriteOpResult } from 'mongoose';
import MongoPaging, { AggregationOutput } from 'mongo-cursor-pagination';
import { TaskModel } from './Task.js';
import { ObjectSchema } from '../schemas/shared/index.js';
Expand All @@ -28,6 +28,7 @@ import {
WRITE_IMAGES_ROLES,
WRITE_COMMENTS_ROLES,
EXPORT_DATA_ROLES,
WRITE_TAGS_ROLES,
} from '../../auth/roles.js';
import {
buildPipeline,
Expand Down Expand Up @@ -475,6 +476,86 @@ export class ImageModel {
}
}

static async createTag(
input: gql.CreateImageTagInput,
context: Pick<Context, 'user'>,
): Promise<{ tags: mongoose.Types.ObjectId[] }> {
try {
const image = await ImageModel.queryById(input.imageId, context);

if (!image.tags) {
image.tags = [] as any as mongoose.Types.DocumentArray<mongoose.Types.ObjectId>;
}

image.tags.push(new mongoose.Types.ObjectId(input.tagId));
await image.save();

return { tags: image.tags };
} catch (err) {
if (err instanceof GraphQLError) throw err;
throw new InternalServerError(err as string);
}
}

static async deleteTag(
input: gql.DeleteImageTagInput,
context: Pick<Context, 'user'>,
): Promise<{ tags: mongoose.Types.ObjectId[] }> {
try {
const image = await ImageModel.queryById(input.imageId, context);

const tag = image.tags?.filter((t) => idMatch(t, input.tagId))[0];
if (!tag) throw new NotFoundError('Tag not found on image');

image.tags = image.tags.filter(
(t) => !idMatch(t, input.tagId),
) as mongoose.Types.ObjectId[];

await image.save();

return { tags: image.tags };
} catch (err) {
if (err instanceof GraphQLError) throw err;
throw new InternalServerError(err as string);
}
}

static async countProjectTag(
input: { tagId: string },
context: Pick<Context, 'user'>,
): Promise<number> {
try {
const projectId = context.user['curr_project']!;
const count = await Image.countDocuments({
projectId: projectId,
tags: new ObjectId(input.tagId)
});

return count;
} catch (err) {
if (err instanceof GraphQLError) throw err;
throw new InternalServerError(err as string);
}
}

static async deleteProjectTag(
input: { tagId: string },
context: Pick<Context, 'user'>,
): Promise<UpdateWriteOpResult> {
try {
const projectId = context.user['curr_project']!;
const res = await Image.updateMany({
projectId: projectId
}, {
$pull: { tags: new mongoose.Types.ObjectId(input.tagId) }
});
return res;
} catch (err) {
if (err instanceof GraphQLError) throw err;
throw new InternalServerError(err as string);
}
}

/**
* Finds Image records and creates new Object subdocuments on them
* It's used by frontend when creating new empty objects and when adding
Expand Down Expand Up @@ -1160,6 +1241,16 @@ export default class AuthedImageModel extends BaseAuthedModel {
return ImageModel.queryByFilter(...args);
}

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

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

@roleCheck(WRITE_COMMENTS_ROLES)
createComment(...args: MethodParams<typeof ImageModel.createComment>) {
return ImageModel.createComment(...args);
Expand Down
Loading

0 comments on commit 6353c55

Please sign in to comment.