feat(server): comment service and resolver (#12761)

close CLOUD-227
close CLOUD-230

































#### PR Dependency Tree


* **PR #12761** 👈
  * **PR #12924**
    * **PR #12925**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced a comprehensive commenting system, enabling users to
create, update, resolve, and delete comments and replies on documents.
* Added support for uploading attachments to comments, with clear error
messaging if size limits are exceeded.
* Implemented role-based permissions for comment actions, including a
new "Commenter" role.
* Enabled paginated listing and change tracking of comments and replies
via GraphQL queries.
* Provided full localization and error handling for comment-related
actions.

* **Bug Fixes**
* Improved uniqueness handling when fetching user data for comments and
replies.

* **Documentation**
* Extended GraphQL schema and frontend localization to document and
support new comment features.

* **Tests**
* Added extensive backend test suites covering all comment and reply
functionalities, permissions, and attachment uploads.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fengmk2
2025-07-01 21:12:28 +08:00
committed by GitHub
parent 6a04fbe335
commit 2aa5c13082
37 changed files with 3504 additions and 9 deletions
@@ -0,0 +1,22 @@
query listCommentChanges($workspaceId: String!, $docId: String!, $pagination: PaginationInput!) {
workspace(id: $workspaceId) {
commentChanges(docId: $docId, pagination: $pagination) {
totalCount
edges {
cursor
node {
action
id
commentId
item
}
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
}
@@ -0,0 +1,26 @@
mutation createComment($input: CommentCreateInput!) {
createComment(input: $input) {
id
content
resolved
createdAt
updatedAt
user {
id
name
avatarUrl
}
replies {
commentId
id
content
createdAt
updatedAt
user {
id
name
avatarUrl
}
}
}
}
@@ -0,0 +1,3 @@
mutation deleteComment($id: String!) {
deleteComment(id: $id)
}
@@ -0,0 +1,40 @@
query listComments($workspaceId: String!, $docId: String!, $pagination: PaginationInput) {
workspace(id: $workspaceId) {
comments(docId: $docId, pagination: $pagination) {
totalCount
edges {
cursor
node {
id
content
resolved
createdAt
updatedAt
user {
id
name
avatarUrl
}
replies {
commentId
id
content
createdAt
updatedAt
user {
id
name
avatarUrl
}
}
}
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
}
@@ -0,0 +1,14 @@
mutation createReply($input: ReplyCreateInput!) {
createReply(input: $input) {
commentId
id
content
createdAt
updatedAt
user {
id
name
avatarUrl
}
}
}
@@ -0,0 +1,3 @@
mutation deleteReply($id: String!) {
deleteReply(id: $id)
}
@@ -0,0 +1,3 @@
mutation updateReply($input: ReplyUpdateInput!) {
updateReply(input: $input)
}
@@ -0,0 +1,3 @@
mutation resolveComment($input: CommentResolveInput!) {
resolveComment(input: $input)
}
@@ -0,0 +1,3 @@
mutation updateComment($input: CommentUpdateInput!) {
updateComment(input: $input)
}
@@ -0,0 +1,3 @@
mutation uploadCommentAttachment($workspaceId: String!, $docId: String!, $attachment: Upload!) {
uploadCommentAttachment(workspaceId: $workspaceId, docId: $docId, attachment: $attachment)
}
@@ -334,6 +334,181 @@ export const changePasswordMutation = {
}`,
};
export const listCommentChangesQuery = {
id: 'listCommentChangesQuery' as const,
op: 'listCommentChanges',
query: `query listCommentChanges($workspaceId: String!, $docId: String!, $pagination: PaginationInput!) {
workspace(id: $workspaceId) {
commentChanges(docId: $docId, pagination: $pagination) {
totalCount
edges {
cursor
node {
action
id
commentId
item
}
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
}`,
};
export const createCommentMutation = {
id: 'createCommentMutation' as const,
op: 'createComment',
query: `mutation createComment($input: CommentCreateInput!) {
createComment(input: $input) {
id
content
resolved
createdAt
updatedAt
user {
id
name
avatarUrl
}
replies {
commentId
id
content
createdAt
updatedAt
user {
id
name
avatarUrl
}
}
}
}`,
};
export const deleteCommentMutation = {
id: 'deleteCommentMutation' as const,
op: 'deleteComment',
query: `mutation deleteComment($id: String!) {
deleteComment(id: $id)
}`,
};
export const listCommentsQuery = {
id: 'listCommentsQuery' as const,
op: 'listComments',
query: `query listComments($workspaceId: String!, $docId: String!, $pagination: PaginationInput) {
workspace(id: $workspaceId) {
comments(docId: $docId, pagination: $pagination) {
totalCount
edges {
cursor
node {
id
content
resolved
createdAt
updatedAt
user {
id
name
avatarUrl
}
replies {
commentId
id
content
createdAt
updatedAt
user {
id
name
avatarUrl
}
}
}
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
}`,
};
export const createReplyMutation = {
id: 'createReplyMutation' as const,
op: 'createReply',
query: `mutation createReply($input: ReplyCreateInput!) {
createReply(input: $input) {
commentId
id
content
createdAt
updatedAt
user {
id
name
avatarUrl
}
}
}`,
};
export const deleteReplyMutation = {
id: 'deleteReplyMutation' as const,
op: 'deleteReply',
query: `mutation deleteReply($id: String!) {
deleteReply(id: $id)
}`,
};
export const updateReplyMutation = {
id: 'updateReplyMutation' as const,
op: 'updateReply',
query: `mutation updateReply($input: ReplyUpdateInput!) {
updateReply(input: $input)
}`,
};
export const resolveCommentMutation = {
id: 'resolveCommentMutation' as const,
op: 'resolveComment',
query: `mutation resolveComment($input: CommentResolveInput!) {
resolveComment(input: $input)
}`,
};
export const updateCommentMutation = {
id: 'updateCommentMutation' as const,
op: 'updateComment',
query: `mutation updateComment($input: CommentUpdateInput!) {
updateComment(input: $input)
}`,
};
export const uploadCommentAttachmentMutation = {
id: 'uploadCommentAttachmentMutation' as const,
op: 'uploadCommentAttachment',
query: `mutation uploadCommentAttachment($workspaceId: String!, $docId: String!, $attachment: Upload!) {
uploadCommentAttachment(
workspaceId: $workspaceId
docId: $docId
attachment: $attachment
)
}`,
file: true,
};
export const addContextCategoryMutation = {
id: 'addContextCategoryMutation' as const,
op: 'addContextCategory',
+423
View File
@@ -140,6 +140,68 @@ export interface ChatMessage {
streamObjects: Maybe<Array<StreamObject>>;
}
/** Comment change action */
export enum CommentChangeAction {
delete = 'delete',
update = 'update',
}
export interface CommentChangeObjectType {
__typename?: 'CommentChangeObjectType';
/** The action of the comment change */
action: CommentChangeAction;
commentId: Maybe<Scalars['ID']['output']>;
id: Scalars['ID']['output'];
/** The item of the comment or reply, different types have different fields, see UnionCommentObjectType */
item: Scalars['JSONObject']['output'];
}
export interface CommentChangeObjectTypeEdge {
__typename?: 'CommentChangeObjectTypeEdge';
cursor: Scalars['String']['output'];
node: CommentChangeObjectType;
}
export interface CommentCreateInput {
content: Scalars['JSONObject']['input'];
docId: Scalars['ID']['input'];
workspaceId: Scalars['ID']['input'];
}
export interface CommentObjectType {
__typename?: 'CommentObjectType';
/** The content of the comment */
content: Scalars['JSONObject']['output'];
/** The created at time of the comment */
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
/** The replies of the comment */
replies: Array<ReplyObjectType>;
/** Whether the comment is resolved */
resolved: Scalars['Boolean']['output'];
/** The updated at time of the comment */
updatedAt: Scalars['DateTime']['output'];
/** The user who created the comment */
user: PublicUserType;
}
export interface CommentObjectTypeEdge {
__typename?: 'CommentObjectTypeEdge';
cursor: Scalars['String']['output'];
node: CommentObjectType;
}
export interface CommentResolveInput {
id: Scalars['ID']['input'];
/** Whether the comment is resolved */
resolved: Scalars['Boolean']['input'];
}
export interface CommentUpdateInput {
content: Scalars['JSONObject']['input'];
id: Scalars['ID']['input'];
}
export enum ContextCategories {
Collection = 'Collection',
Tag = 'Tag',
@@ -565,6 +627,10 @@ export interface DocNotFoundDataType {
export interface DocPermissions {
__typename?: 'DocPermissions';
Doc_Comments_Create: Scalars['Boolean']['output'];
Doc_Comments_Delete: Scalars['Boolean']['output'];
Doc_Comments_Read: Scalars['Boolean']['output'];
Doc_Comments_Resolve: Scalars['Boolean']['output'];
Doc_Copy: Scalars['Boolean']['output'];
Doc_Delete: Scalars['Boolean']['output'];
Doc_Duplicate: Scalars['Boolean']['output'];
@@ -582,6 +648,7 @@ export interface DocPermissions {
/** User permission in doc */
export enum DocRole {
Commenter = 'Commenter',
Editor = 'Editor',
External = 'External',
Manager = 'Manager',
@@ -710,6 +777,7 @@ export enum ErrorNames {
CAN_NOT_REVOKE_YOURSELF = 'CAN_NOT_REVOKE_YOURSELF',
CAPTCHA_VERIFICATION_FAILED = 'CAPTCHA_VERIFICATION_FAILED',
COMMENT_ATTACHMENT_NOT_FOUND = 'COMMENT_ATTACHMENT_NOT_FOUND',
COMMENT_ATTACHMENT_QUOTA_EXCEEDED = 'COMMENT_ATTACHMENT_QUOTA_EXCEEDED',
COMMENT_NOT_FOUND = 'COMMENT_NOT_FOUND',
COPILOT_ACTION_TAKEN = 'COPILOT_ACTION_TAKEN',
COPILOT_CONTEXT_FILE_NOT_SUPPORTED = 'COPILOT_CONTEXT_FILE_NOT_SUPPORTED',
@@ -1248,6 +1316,7 @@ export interface Mutation {
createChangePasswordUrl: Scalars['String']['output'];
/** Create a subscription checkout link of stripe */
createCheckoutSession: Scalars['String']['output'];
createComment: CommentObjectType;
/** Create a context session */
createCopilotContext: Scalars['String']['output'];
/** Create a chat message */
@@ -1259,6 +1328,7 @@ export interface Mutation {
/** Create a stripe customer portal to manage payment methods */
createCustomerPortal: Scalars['String']['output'];
createInviteLink: InviteLink;
createReply: ReplyObjectType;
createSelfhostWorkspaceCustomerPortal: Scalars['String']['output'];
/** Create a new user */
createUser: UserType;
@@ -1267,6 +1337,10 @@ export interface Mutation {
deactivateLicense: Scalars['Boolean']['output'];
deleteAccount: DeleteAccount;
deleteBlob: Scalars['Boolean']['output'];
/** Delete a comment */
deleteComment: Scalars['Boolean']['output'];
/** Delete a reply */
deleteReply: Scalars['Boolean']['output'];
/** Delete a user account */
deleteUser: DeleteAccount;
deleteWorkspace: Scalars['Boolean']['output'];
@@ -1306,6 +1380,8 @@ export interface Mutation {
/** Remove workspace embedding files */
removeWorkspaceEmbeddingFiles: Scalars['Boolean']['output'];
removeWorkspaceFeature: Scalars['Boolean']['output'];
/** Resolve a comment or not */
resolveComment: Scalars['Boolean']['output'];
resumeSubscription: SubscriptionType;
retryAudioTranscription: Maybe<TranscriptionResultType>;
/** @deprecated use [revokeMember] instead */
@@ -1326,6 +1402,8 @@ export interface Mutation {
submitAudioTranscription: Maybe<TranscriptionResultType>;
/** update app configuration */
updateAppConfig: Scalars['JSONObject']['output'];
/** Update a comment content */
updateComment: Scalars['Boolean']['output'];
/** Update a copilot prompt */
updateCopilotPrompt: CopilotPromptType;
/** Update a chat session */
@@ -1333,6 +1411,8 @@ export interface Mutation {
updateDocDefaultRole: Scalars['Boolean']['output'];
updateDocUserRole: Scalars['Boolean']['output'];
updateProfile: UserType;
/** Update a reply content */
updateReply: Scalars['Boolean']['output'];
/** Update user settings */
updateSettings: Scalars['Boolean']['output'];
updateSubscriptionRecurring: SubscriptionType;
@@ -1346,6 +1426,8 @@ export interface Mutation {
updateWorkspaceEmbeddingIgnoredDocs: Scalars['Int']['output'];
/** Upload user avatar */
uploadAvatar: UserType;
/** Upload a comment attachment and return the access url */
uploadCommentAttachment: Scalars['String']['output'];
/** validate app configuration */
validateAppConfig: Array<AppConfigValidateResult>;
verifyEmail: Scalars['Boolean']['output'];
@@ -1428,6 +1510,10 @@ export interface MutationCreateCheckoutSessionArgs {
input: CreateCheckoutSessionInput;
}
export interface MutationCreateCommentArgs {
input: CommentCreateInput;
}
export interface MutationCreateCopilotContextArgs {
sessionId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
@@ -1450,6 +1536,10 @@ export interface MutationCreateInviteLinkArgs {
workspaceId: Scalars['String']['input'];
}
export interface MutationCreateReplyArgs {
input: ReplyCreateInput;
}
export interface MutationCreateSelfhostWorkspaceCustomerPortalArgs {
workspaceId: Scalars['String']['input'];
}
@@ -1473,6 +1563,14 @@ export interface MutationDeleteBlobArgs {
workspaceId: Scalars['String']['input'];
}
export interface MutationDeleteCommentArgs {
id: Scalars['String']['input'];
}
export interface MutationDeleteReplyArgs {
id: Scalars['String']['input'];
}
export interface MutationDeleteUserArgs {
id: Scalars['String']['input'];
}
@@ -1586,6 +1684,10 @@ export interface MutationRemoveWorkspaceFeatureArgs {
workspaceId: Scalars['String']['input'];
}
export interface MutationResolveCommentArgs {
input: CommentResolveInput;
}
export interface MutationResumeSubscriptionArgs {
idempotencyKey?: InputMaybe<Scalars['String']['input']>;
plan?: InputMaybe<SubscriptionPlan>;
@@ -1670,6 +1772,10 @@ export interface MutationUpdateAppConfigArgs {
updates: Array<UpdateAppConfigInput>;
}
export interface MutationUpdateCommentArgs {
input: CommentUpdateInput;
}
export interface MutationUpdateCopilotPromptArgs {
messages: Array<CopilotPromptMessageInput>;
name: Scalars['String']['input'];
@@ -1691,6 +1797,10 @@ export interface MutationUpdateProfileArgs {
input: UpdateUserInput;
}
export interface MutationUpdateReplyArgs {
input: ReplyUpdateInput;
}
export interface MutationUpdateSettingsArgs {
input: UpdateUserSettingsInput;
}
@@ -1726,6 +1836,12 @@ export interface MutationUploadAvatarArgs {
avatar: Scalars['Upload']['input'];
}
export interface MutationUploadCommentAttachmentArgs {
attachment: Scalars['Upload']['input'];
docId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
}
export interface MutationValidateAppConfigArgs {
updates: Array<UpdateAppConfigInput>;
}
@@ -1814,6 +1930,20 @@ export interface PageInfo {
startCursor: Maybe<Scalars['String']['output']>;
}
export interface PaginatedCommentChangeObjectType {
__typename?: 'PaginatedCommentChangeObjectType';
edges: Array<CommentChangeObjectTypeEdge>;
pageInfo: PageInfo;
totalCount: Scalars['Int']['output'];
}
export interface PaginatedCommentObjectType {
__typename?: 'PaginatedCommentObjectType';
edges: Array<CommentObjectTypeEdge>;
pageInfo: PageInfo;
totalCount: Scalars['Int']['output'];
}
export interface PaginatedCopilotWorkspaceFileType {
__typename?: 'PaginatedCopilotWorkspaceFileType';
edges: Array<CopilotWorkspaceFileTypeEdge>;
@@ -2038,6 +2168,30 @@ export interface RemoveContextFileInput {
fileId: Scalars['String']['input'];
}
export interface ReplyCreateInput {
commentId: Scalars['ID']['input'];
content: Scalars['JSONObject']['input'];
}
export interface ReplyObjectType {
__typename?: 'ReplyObjectType';
commentId: Scalars['ID']['output'];
/** The content of the reply */
content: Scalars['JSONObject']['output'];
/** The created at time of the reply */
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
/** The updated at time of the reply */
updatedAt: Scalars['DateTime']['output'];
/** The user who created the reply */
user: PublicUserType;
}
export interface ReplyUpdateInput {
content: Scalars['JSONObject']['input'];
id: Scalars['ID']['input'];
}
export interface RevokeDocUserRoleInput {
docId: Scalars['String']['input'];
userId: Scalars['String']['input'];
@@ -2605,6 +2759,10 @@ export interface WorkspaceType {
blobs: Array<ListedBlob>;
/** Blobs size of workspace */
blobsSize: Scalars['Int']['output'];
/** Get comment changes of a doc */
commentChanges: PaginatedCommentChangeObjectType;
/** Get comments of a doc */
comments: PaginatedCommentObjectType;
/** Workspace created date */
createdAt: Scalars['DateTime']['output'];
/** Get get with given id */
@@ -2672,6 +2830,16 @@ export interface WorkspaceTypeAggregateArgs {
input: AggregateInput;
}
export interface WorkspaceTypeCommentChangesArgs {
docId: Scalars['String']['input'];
pagination: PaginationInput;
}
export interface WorkspaceTypeCommentsArgs {
docId: Scalars['String']['input'];
pagination?: InputMaybe<PaginationInput>;
}
export interface WorkspaceTypeDocArgs {
docId: Scalars['String']['input'];
}
@@ -3069,6 +3237,211 @@ export type ChangePasswordMutation = {
changePassword: boolean;
};
export type ListCommentChangesQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
docId: Scalars['String']['input'];
pagination: PaginationInput;
}>;
export type ListCommentChangesQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
commentChanges: {
__typename?: 'PaginatedCommentChangeObjectType';
totalCount: number;
edges: Array<{
__typename?: 'CommentChangeObjectTypeEdge';
cursor: string;
node: {
__typename?: 'CommentChangeObjectType';
action: CommentChangeAction;
id: string;
commentId: string | null;
item: any;
};
}>;
pageInfo: {
__typename?: 'PageInfo';
startCursor: string | null;
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
};
};
};
};
export type CreateCommentMutationVariables = Exact<{
input: CommentCreateInput;
}>;
export type CreateCommentMutation = {
__typename?: 'Mutation';
createComment: {
__typename?: 'CommentObjectType';
id: string;
content: any;
resolved: boolean;
createdAt: string;
updatedAt: string;
user: {
__typename?: 'PublicUserType';
id: string;
name: string;
avatarUrl: string | null;
};
replies: Array<{
__typename?: 'ReplyObjectType';
commentId: string;
id: string;
content: any;
createdAt: string;
updatedAt: string;
user: {
__typename?: 'PublicUserType';
id: string;
name: string;
avatarUrl: string | null;
};
}>;
};
};
export type DeleteCommentMutationVariables = Exact<{
id: Scalars['String']['input'];
}>;
export type DeleteCommentMutation = {
__typename?: 'Mutation';
deleteComment: boolean;
};
export type ListCommentsQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
docId: Scalars['String']['input'];
pagination?: InputMaybe<PaginationInput>;
}>;
export type ListCommentsQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
comments: {
__typename?: 'PaginatedCommentObjectType';
totalCount: number;
edges: Array<{
__typename?: 'CommentObjectTypeEdge';
cursor: string;
node: {
__typename?: 'CommentObjectType';
id: string;
content: any;
resolved: boolean;
createdAt: string;
updatedAt: string;
user: {
__typename?: 'PublicUserType';
id: string;
name: string;
avatarUrl: string | null;
};
replies: Array<{
__typename?: 'ReplyObjectType';
commentId: string;
id: string;
content: any;
createdAt: string;
updatedAt: string;
user: {
__typename?: 'PublicUserType';
id: string;
name: string;
avatarUrl: string | null;
};
}>;
};
}>;
pageInfo: {
__typename?: 'PageInfo';
startCursor: string | null;
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
};
};
};
};
export type CreateReplyMutationVariables = Exact<{
input: ReplyCreateInput;
}>;
export type CreateReplyMutation = {
__typename?: 'Mutation';
createReply: {
__typename?: 'ReplyObjectType';
commentId: string;
id: string;
content: any;
createdAt: string;
updatedAt: string;
user: {
__typename?: 'PublicUserType';
id: string;
name: string;
avatarUrl: string | null;
};
};
};
export type DeleteReplyMutationVariables = Exact<{
id: Scalars['String']['input'];
}>;
export type DeleteReplyMutation = {
__typename?: 'Mutation';
deleteReply: boolean;
};
export type UpdateReplyMutationVariables = Exact<{
input: ReplyUpdateInput;
}>;
export type UpdateReplyMutation = {
__typename?: 'Mutation';
updateReply: boolean;
};
export type ResolveCommentMutationVariables = Exact<{
input: CommentResolveInput;
}>;
export type ResolveCommentMutation = {
__typename?: 'Mutation';
resolveComment: boolean;
};
export type UpdateCommentMutationVariables = Exact<{
input: CommentUpdateInput;
}>;
export type UpdateCommentMutation = {
__typename?: 'Mutation';
updateComment: boolean;
};
export type UploadCommentAttachmentMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
docId: Scalars['String']['input'];
attachment: Scalars['Upload']['input'];
}>;
export type UploadCommentAttachmentMutation = {
__typename?: 'Mutation';
uploadCommentAttachment: string;
};
export type AddContextCategoryMutationVariables = Exact<{
options: AddContextCategoryInput;
}>;
@@ -5340,6 +5713,16 @@ export type Queries =
variables: ListBlobsQueryVariables;
response: ListBlobsQuery;
}
| {
name: 'listCommentChangesQuery';
variables: ListCommentChangesQueryVariables;
response: ListCommentChangesQuery;
}
| {
name: 'listCommentsQuery';
variables: ListCommentsQueryVariables;
response: ListCommentsQuery;
}
| {
name: 'listContextObjectQuery';
variables: ListContextObjectQueryVariables;
@@ -5737,6 +6120,46 @@ export type Mutations =
variables: ChangePasswordMutationVariables;
response: ChangePasswordMutation;
}
| {
name: 'createCommentMutation';
variables: CreateCommentMutationVariables;
response: CreateCommentMutation;
}
| {
name: 'deleteCommentMutation';
variables: DeleteCommentMutationVariables;
response: DeleteCommentMutation;
}
| {
name: 'createReplyMutation';
variables: CreateReplyMutationVariables;
response: CreateReplyMutation;
}
| {
name: 'deleteReplyMutation';
variables: DeleteReplyMutationVariables;
response: DeleteReplyMutation;
}
| {
name: 'updateReplyMutation';
variables: UpdateReplyMutationVariables;
response: UpdateReplyMutation;
}
| {
name: 'resolveCommentMutation';
variables: ResolveCommentMutationVariables;
response: ResolveCommentMutation;
}
| {
name: 'updateCommentMutation';
variables: UpdateCommentMutationVariables;
response: UpdateCommentMutation;
}
| {
name: 'uploadCommentAttachmentMutation';
variables: UploadCommentAttachmentMutationVariables;
response: UploadCommentAttachmentMutation;
}
| {
name: 'addContextCategoryMutation';
variables: AddContextCategoryMutationVariables;