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
+138
View File
@@ -99,6 +99,73 @@ type ChatMessage {
streamObjects: [StreamObject!]
}
"""Comment change action"""
enum CommentChangeAction {
delete
update
}
type CommentChangeObjectType {
"""The action of the comment change"""
action: CommentChangeAction!
commentId: ID
id: ID!
"""
The item of the comment or reply, different types have different fields, see UnionCommentObjectType
"""
item: JSONObject!
}
type CommentChangeObjectTypeEdge {
cursor: String!
node: CommentChangeObjectType!
}
input CommentCreateInput {
content: JSONObject!
docId: ID!
workspaceId: ID!
}
type CommentObjectType {
"""The content of the comment"""
content: JSONObject!
"""The created at time of the comment"""
createdAt: DateTime!
id: ID!
"""The replies of the comment"""
replies: [ReplyObjectType!]!
"""Whether the comment is resolved"""
resolved: Boolean!
"""The updated at time of the comment"""
updatedAt: DateTime!
"""The user who created the comment"""
user: PublicUserType!
}
type CommentObjectTypeEdge {
cursor: String!
node: CommentObjectType!
}
input CommentResolveInput {
id: ID!
"""Whether the comment is resolved"""
resolved: Boolean!
}
input CommentUpdateInput {
content: JSONObject!
id: ID!
}
enum ContextCategories {
Collection
Tag
@@ -456,6 +523,10 @@ type DocNotFoundDataType {
}
type DocPermissions {
Doc_Comments_Create: Boolean!
Doc_Comments_Delete: Boolean!
Doc_Comments_Read: Boolean!
Doc_Comments_Resolve: Boolean!
Doc_Copy: Boolean!
Doc_Delete: Boolean!
Doc_Duplicate: Boolean!
@@ -473,6 +544,7 @@ type DocPermissions {
"""User permission in doc"""
enum DocRole {
Commenter
Editor
External
Manager
@@ -541,6 +613,7 @@ enum ErrorNames {
CAN_NOT_REVOKE_YOURSELF
CAPTCHA_VERIFICATION_FAILED
COMMENT_ATTACHMENT_NOT_FOUND
COMMENT_ATTACHMENT_QUOTA_EXCEEDED
COMMENT_NOT_FOUND
COPILOT_ACTION_TAKEN
COPILOT_CONTEXT_FILE_NOT_SUPPORTED
@@ -1090,6 +1163,7 @@ type Mutation {
"""Create a subscription checkout link of stripe"""
createCheckoutSession(input: CreateCheckoutSessionInput!): String!
createComment(input: CommentCreateInput!): CommentObjectType!
"""Create a context session"""
createCopilotContext(sessionId: String!, workspaceId: String!): String!
@@ -1106,6 +1180,7 @@ type Mutation {
"""Create a stripe customer portal to manage payment methods"""
createCustomerPortal: String!
createInviteLink(expireTime: WorkspaceInviteLinkExpireTime!, workspaceId: String!): InviteLink!
createReply(input: ReplyCreateInput!): ReplyObjectType!
createSelfhostWorkspaceCustomerPortal(workspaceId: String!): String!
"""Create a new user"""
@@ -1117,6 +1192,12 @@ type Mutation {
deleteAccount: DeleteAccount!
deleteBlob(hash: String @deprecated(reason: "use parameter [key]"), key: String, permanently: Boolean! = false, workspaceId: String!): Boolean!
"""Delete a comment"""
deleteComment(id: String!): Boolean!
"""Delete a reply"""
deleteReply(id: String!): Boolean!
"""Delete a user account"""
deleteUser(id: String!): DeleteAccount!
deleteWorkspace(id: String!): Boolean!
@@ -1165,6 +1246,9 @@ type Mutation {
"""Remove workspace embedding files"""
removeWorkspaceEmbeddingFiles(fileId: String!, workspaceId: String!): Boolean!
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Boolean!
"""Resolve a comment or not"""
resolveComment(input: CommentResolveInput!): Boolean!
resumeSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType!
retryAudioTranscription(jobId: String!, workspaceId: String!): TranscriptionResultType
revoke(userId: String!, workspaceId: String!): Boolean! @deprecated(reason: "use [revokeMember] instead")
@@ -1185,6 +1269,9 @@ type Mutation {
"""update app configuration"""
updateAppConfig(updates: [UpdateAppConfigInput!]!): JSONObject!
"""Update a comment content"""
updateComment(input: CommentUpdateInput!): Boolean!
"""Update a copilot prompt"""
updateCopilotPrompt(messages: [CopilotPromptMessageInput!]!, name: String!): CopilotPromptType!
@@ -1194,6 +1281,9 @@ type Mutation {
updateDocUserRole(input: UpdateDocUserRoleInput!): Boolean!
updateProfile(input: UpdateUserInput!): UserType!
"""Update a reply content"""
updateReply(input: ReplyUpdateInput!): Boolean!
"""Update user settings"""
updateSettings(input: UpdateUserSettingsInput!): Boolean!
updateSubscriptionRecurring(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, recurring: SubscriptionRecurring!, workspaceId: String): SubscriptionType!
@@ -1213,6 +1303,9 @@ type Mutation {
"""Upload user avatar"""
uploadAvatar(avatar: Upload!): UserType!
"""Upload a comment attachment and return the access url"""
uploadCommentAttachment(attachment: Upload!, docId: String!, workspaceId: String!): String!
"""validate app configuration"""
validateAppConfig(updates: [UpdateAppConfigInput!]!): [AppConfigValidateResult!]!
verifyEmail(token: String!): Boolean!
@@ -1301,6 +1394,18 @@ type PageInfo {
startCursor: String
}
type PaginatedCommentChangeObjectType {
edges: [CommentChangeObjectTypeEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PaginatedCommentObjectType {
edges: [CommentObjectTypeEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PaginatedCopilotWorkspaceFileType {
edges: [CopilotWorkspaceFileTypeEdge!]!
pageInfo: PageInfo!
@@ -1474,6 +1579,33 @@ input RemoveContextFileInput {
fileId: String!
}
input ReplyCreateInput {
commentId: ID!
content: JSONObject!
}
type ReplyObjectType {
commentId: ID!
"""The content of the reply"""
content: JSONObject!
"""The created at time of the reply"""
createdAt: DateTime!
id: ID!
"""The updated at time of the reply"""
updatedAt: DateTime!
"""The user who created the reply"""
user: PublicUserType!
}
input ReplyUpdateInput {
content: JSONObject!
id: ID!
}
input RevokeDocUserRoleInput {
docId: String!
userId: String!
@@ -2017,6 +2149,12 @@ type WorkspaceType {
"""Blobs size of workspace"""
blobsSize: Int!
"""Get comment changes of a doc"""
commentChanges(docId: String!, pagination: PaginationInput!): PaginatedCommentChangeObjectType!
"""Get comments of a doc"""
comments(docId: String!, pagination: PaginationInput): PaginatedCommentObjectType!
"""Workspace created date"""
createdAt: DateTime!