feat(server): add blocked state to workspace docs (#10585)

close CLOUD-162
This commit is contained in:
fengmk2
2025-03-05 12:49:33 +00:00
parent 70a0337ea3
commit 43ded6aa38
6 changed files with 55 additions and 9 deletions

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "workspace_pages" ADD COLUMN "blocked" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -124,6 +124,8 @@ model WorkspaceDoc {
defaultRole Int @default(30) @db.SmallInt defaultRole Int @default(30) @db.SmallInt
// Page/Edgeless // Page/Edgeless
mode Int @default(0) @db.SmallInt mode Int @default(0) @db.SmallInt
// Whether the doc is blocked
blocked Boolean @default(false)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)

View File

@@ -439,6 +439,12 @@ export const USER_FRIENDLY_ERRORS = {
message: ({ docId, action }) => message: ({ docId, action }) =>
`You do not have permission to perform ${action} action on doc ${docId}.`, `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: { version_rejected: {
type: 'action_forbidden', type: 'action_forbidden',
args: { version: 'string', serverVersion: 'string' }, args: { version: 'string', serverVersion: 'string' },

View File

@@ -334,6 +334,17 @@ export class DocActionDenied extends UserFriendlyError {
} }
} }
@ObjectType() @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 { class VersionRejectedDataType {
@Field() version!: string @Field() version!: string
@Field() serverVersion!: string @Field() serverVersion!: string
@@ -871,6 +882,7 @@ export enum ErrorNames {
CAN_NOT_REVOKE_YOURSELF, CAN_NOT_REVOKE_YOURSELF,
DOC_NOT_FOUND, DOC_NOT_FOUND,
DOC_ACTION_DENIED, DOC_ACTION_DENIED,
DOC_UPDATE_BLOCKED,
VERSION_REJECTED, VERSION_REJECTED,
INVALID_HISTORY_TIMESTAMP, INVALID_HISTORY_TIMESTAMP,
DOC_HISTORY_NOT_FOUND, DOC_HISTORY_NOT_FOUND,
@@ -942,5 +954,5 @@ registerEnumType(ErrorNames, {
export const ErrorDataUnionType = createUnionType({ export const ErrorDataUnionType = createUnionType({
name: 'ErrorDataUnion', name: 'ErrorDataUnion',
types: () => 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,
}); });

View File

@@ -13,6 +13,7 @@ import { Socket } from 'socket.io';
import { import {
CallMetric, CallMetric,
DocNotFound, DocNotFound,
DocUpdateBlocked,
GatewayErrorWrapper, GatewayErrorWrapper,
metrics, metrics,
NotInSpace, NotInSpace,
@@ -20,6 +21,7 @@ import {
SpaceAccessDenied, SpaceAccessDenied,
VersionRejected, VersionRejected,
} from '../../base'; } from '../../base';
import { Models } from '../../models';
import { CurrentUser } from '../auth'; import { CurrentUser } from '../auth';
import { import {
DocReader, DocReader,
@@ -147,7 +149,8 @@ export class SpaceSyncGateway
private readonly ac: AccessController, private readonly ac: AccessController,
private readonly workspace: PgWorkspaceDocStorageAdapter, private readonly workspace: PgWorkspaceDocStorageAdapter,
private readonly userspace: PgUserspaceDocStorageAdapter, private readonly userspace: PgUserspaceDocStorageAdapter,
private readonly docReader: DocReader private readonly docReader: DocReader,
private readonly models: Models
) {} ) {}
handleConnection() { handleConnection() {
@@ -171,7 +174,8 @@ export class SpaceSyncGateway
client, client,
this.workspace, this.workspace,
this.ac, this.ac,
this.docReader this.docReader,
this.models
); );
const userspace = new UserspaceSyncAdapter(client, this.userspace); const userspace = new UserspaceSyncAdapter(client, this.userspace);
@@ -501,10 +505,15 @@ abstract class SyncSocketAdapter {
action: WorkspaceAction action: WorkspaceAction
): Promise<void>; ): Promise<void>;
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 // TODO(@forehalo): enable this after 0.19 goes out of life
// this.assertIn(spaceId); // 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) { diff(spaceId: string, docId: string, stateVector?: Uint8Array) {
@@ -528,18 +537,27 @@ class WorkspaceSyncAdapter extends SyncSocketAdapter {
client: Socket, client: Socket,
storage: DocStorageAdapter, storage: DocStorageAdapter,
private readonly ac: AccessController, private readonly ac: AccessController,
private readonly docReader: DocReader private readonly docReader: DocReader,
private readonly models: Models
) { ) {
super(SpaceType.Workspace, client, storage); super(SpaceType.Workspace, client, storage);
} }
override push( override async push(
spaceId: string, spaceId: string,
docId: string, docId: string,
updates: Buffer[], updates: Buffer[],
editorId: string 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( override async diff(

View File

@@ -319,12 +319,17 @@ type DocType {
workspaceId: String! workspaceId: String!
} }
type DocUpdateBlockedDataType {
docId: String!
spaceId: String!
}
type EditorType { type EditorType {
avatarUrl: String avatarUrl: String
name: 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 { enum ErrorNames {
ACCESS_DENIED ACCESS_DENIED
@@ -362,6 +367,7 @@ enum ErrorNames {
DOC_HISTORY_NOT_FOUND DOC_HISTORY_NOT_FOUND
DOC_IS_NOT_PUBLIC DOC_IS_NOT_PUBLIC
DOC_NOT_FOUND DOC_NOT_FOUND
DOC_UPDATE_BLOCKED
EARLY_ACCESS_REQUIRED EARLY_ACCESS_REQUIRED
EMAIL_ALREADY_USED EMAIL_ALREADY_USED
EMAIL_TOKEN_NOT_FOUND EMAIL_TOKEN_NOT_FOUND