From 43ded6aa388cdf3696075ce6bec1a6a75bd74a0e Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Wed, 5 Mar 2025 12:49:33 +0000 Subject: [PATCH] feat(server): add blocked state to workspace docs (#10585) close CLOUD-162 --- .../migration.sql | 2 ++ packages/backend/server/schema.prisma | 2 ++ packages/backend/server/src/base/error/def.ts | 6 ++++ .../server/src/base/error/errors.gen.ts | 14 +++++++- .../backend/server/src/core/sync/gateway.ts | 32 +++++++++++++++---- packages/backend/server/src/schema.gql | 8 ++++- 6 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 packages/backend/server/migrations/20250303194905_add_blocked_to_workspace_docs/migration.sql diff --git a/packages/backend/server/migrations/20250303194905_add_blocked_to_workspace_docs/migration.sql b/packages/backend/server/migrations/20250303194905_add_blocked_to_workspace_docs/migration.sql new file mode 100644 index 0000000000..66abcd7aeb --- /dev/null +++ b/packages/backend/server/migrations/20250303194905_add_blocked_to_workspace_docs/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "workspace_pages" ADD COLUMN "blocked" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/backend/server/schema.prisma b/packages/backend/server/schema.prisma index 9cefe0c596..8449a60e34 100644 --- a/packages/backend/server/schema.prisma +++ b/packages/backend/server/schema.prisma @@ -124,6 +124,8 @@ model WorkspaceDoc { defaultRole Int @default(30) @db.SmallInt // Page/Edgeless mode Int @default(0) @db.SmallInt + // Whether the doc is blocked + blocked Boolean @default(false) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index 52889b088d..1dc178c0b9 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -439,6 +439,12 @@ export const USER_FRIENDLY_ERRORS = { message: ({ docId, action }) => `You do not have permission to perform ${action} action on doc ${docId}.`, }, + doc_update_blocked: { + type: 'action_forbidden', + args: { spaceId: 'string', docId: 'string' }, + message: ({ spaceId, docId }) => + `Doc ${docId} under Space ${spaceId} is blocked from updating.`, + }, version_rejected: { type: 'action_forbidden', args: { version: 'string', serverVersion: 'string' }, diff --git a/packages/backend/server/src/base/error/errors.gen.ts b/packages/backend/server/src/base/error/errors.gen.ts index 3d081d2cc3..860debf2ce 100644 --- a/packages/backend/server/src/base/error/errors.gen.ts +++ b/packages/backend/server/src/base/error/errors.gen.ts @@ -334,6 +334,17 @@ export class DocActionDenied extends UserFriendlyError { } } @ObjectType() +class DocUpdateBlockedDataType { + @Field() spaceId!: string + @Field() docId!: string +} + +export class DocUpdateBlocked extends UserFriendlyError { + constructor(args: DocUpdateBlockedDataType, message?: string | ((args: DocUpdateBlockedDataType) => string)) { + super('action_forbidden', 'doc_update_blocked', message, args); + } +} +@ObjectType() class VersionRejectedDataType { @Field() version!: string @Field() serverVersion!: string @@ -871,6 +882,7 @@ export enum ErrorNames { CAN_NOT_REVOKE_YOURSELF, DOC_NOT_FOUND, DOC_ACTION_DENIED, + DOC_UPDATE_BLOCKED, VERSION_REJECTED, INVALID_HISTORY_TIMESTAMP, DOC_HISTORY_NOT_FOUND, @@ -942,5 +954,5 @@ registerEnumType(ErrorNames, { export const ErrorDataUnionType = createUnionType({ name: 'ErrorDataUnion', types: () => - [GraphqlBadRequestDataType, QueryTooLongDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseUpdateParamsDataType, WorkspaceMembersExceedLimitToDowngradeDataType, UnsupportedClientVersionDataType] as const, + [GraphqlBadRequestDataType, QueryTooLongDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseUpdateParamsDataType, WorkspaceMembersExceedLimitToDowngradeDataType, UnsupportedClientVersionDataType] as const, }); diff --git a/packages/backend/server/src/core/sync/gateway.ts b/packages/backend/server/src/core/sync/gateway.ts index 344753ebbe..7923f1f1dd 100644 --- a/packages/backend/server/src/core/sync/gateway.ts +++ b/packages/backend/server/src/core/sync/gateway.ts @@ -13,6 +13,7 @@ import { Socket } from 'socket.io'; import { CallMetric, DocNotFound, + DocUpdateBlocked, GatewayErrorWrapper, metrics, NotInSpace, @@ -20,6 +21,7 @@ import { SpaceAccessDenied, VersionRejected, } from '../../base'; +import { Models } from '../../models'; import { CurrentUser } from '../auth'; import { DocReader, @@ -147,7 +149,8 @@ export class SpaceSyncGateway private readonly ac: AccessController, private readonly workspace: PgWorkspaceDocStorageAdapter, private readonly userspace: PgUserspaceDocStorageAdapter, - private readonly docReader: DocReader + private readonly docReader: DocReader, + private readonly models: Models ) {} handleConnection() { @@ -171,7 +174,8 @@ export class SpaceSyncGateway client, this.workspace, this.ac, - this.docReader + this.docReader, + this.models ); const userspace = new UserspaceSyncAdapter(client, this.userspace); @@ -501,10 +505,15 @@ abstract class SyncSocketAdapter { action: WorkspaceAction ): Promise; - push(spaceId: string, docId: string, updates: Buffer[], editorId: string) { + async push( + spaceId: string, + docId: string, + updates: Buffer[], + editorId: string + ) { // TODO(@forehalo): enable this after 0.19 goes out of life // this.assertIn(spaceId); - return this.storage.pushDocUpdates(spaceId, docId, updates, editorId); + return await this.storage.pushDocUpdates(spaceId, docId, updates, editorId); } diff(spaceId: string, docId: string, stateVector?: Uint8Array) { @@ -528,18 +537,27 @@ class WorkspaceSyncAdapter extends SyncSocketAdapter { client: Socket, storage: DocStorageAdapter, private readonly ac: AccessController, - private readonly docReader: DocReader + private readonly docReader: DocReader, + private readonly models: Models ) { super(SpaceType.Workspace, client, storage); } - override push( + override async push( spaceId: string, docId: string, updates: Buffer[], editorId: string ) { - return super.push(spaceId, docId, updates, editorId); + const docMeta = await this.models.doc.getMeta(spaceId, docId, { + select: { + blocked: true, + }, + }); + if (docMeta?.blocked) { + throw new DocUpdateBlocked({ spaceId, docId }); + } + return await super.push(spaceId, docId, updates, editorId); } override async diff( diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 7179ef7a38..2a47ec46c0 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -319,12 +319,17 @@ type DocType { workspaceId: String! } +type DocUpdateBlockedDataType { + docId: String! + spaceId: String! +} + type EditorType { avatarUrl: String name: String! } -union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToMatchContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WorkspaceMembersExceedLimitToDowngradeDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType +union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToMatchContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | DocUpdateBlockedDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WorkspaceMembersExceedLimitToDowngradeDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType enum ErrorNames { ACCESS_DENIED @@ -362,6 +367,7 @@ enum ErrorNames { DOC_HISTORY_NOT_FOUND DOC_IS_NOT_PUBLIC DOC_NOT_FOUND + DOC_UPDATE_BLOCKED EARLY_ACCESS_REQUIRED EMAIL_ALREADY_USED EMAIL_TOKEN_NOT_FOUND