diff --git a/packages/backend/server/src/core/doc/adapters/workspace.ts b/packages/backend/server/src/core/doc/adapters/workspace.ts index b9ecb928bb..c4b656beab 100644 --- a/packages/backend/server/src/core/doc/adapters/workspace.ts +++ b/packages/backend/server/src/core/doc/adapters/workspace.ts @@ -219,6 +219,12 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter { const histories = await this.db.snapshotHistory.findMany({ select: { timestamp: true, + createdByUser: { + select: { + name: true, + avatarUrl: true, + }, + }, }, where: { workspaceId, @@ -233,7 +239,10 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter { take: query.limit, }); - return histories.map(h => h.timestamp.getTime()); + return histories.map(h => ({ + timestamp: h.timestamp.getTime(), + editor: h.createdByUser, + })); } async getDocHistory(workspaceId: string, docId: string, timestamp: number) { diff --git a/packages/backend/server/src/core/doc/index.ts b/packages/backend/server/src/core/doc/index.ts index 55698352ca..b0b7cb226e 100644 --- a/packages/backend/server/src/core/doc/index.ts +++ b/packages/backend/server/src/core/doc/index.ts @@ -22,4 +22,4 @@ import { DocStorageOptions } from './options'; export class DocStorageModule {} export { PgUserspaceDocStorageAdapter, PgWorkspaceDocStorageAdapter }; -export { DocStorageAdapter } from './storage'; +export { DocStorageAdapter, type Editor } from './storage'; diff --git a/packages/backend/server/src/core/doc/storage/doc.ts b/packages/backend/server/src/core/doc/storage/doc.ts index 50c79b4199..49099afe7f 100644 --- a/packages/backend/server/src/core/doc/storage/doc.ts +++ b/packages/backend/server/src/core/doc/storage/doc.ts @@ -30,6 +30,11 @@ export interface HistoryFilter { limit?: number; } +export interface Editor { + name: string; + avatarUrl: string | null; +} + export interface DocStorageOptions { mergeUpdates?: (updates: Uint8Array[]) => Promise | Uint8Array; } @@ -132,7 +137,7 @@ export abstract class DocStorageAdapter extends Connection { spaceId: string, docId: string, query: { skip?: number; limit?: number } - ): Promise; + ): Promise<{ timestamp: number; editor: Editor | null }[]>; abstract getDocHistory( spaceId: string, docId: string, diff --git a/packages/backend/server/src/core/doc/storage/index.ts b/packages/backend/server/src/core/doc/storage/index.ts index a69fc46d92..6ba0e23dd1 100644 --- a/packages/backend/server/src/core/doc/storage/index.ts +++ b/packages/backend/server/src/core/doc/storage/index.ts @@ -28,5 +28,6 @@ export { DocStorageAdapter, type DocStorageOptions, type DocUpdate, + type Editor, type HistoryFilter, } from './doc'; diff --git a/packages/backend/server/src/core/workspaces/resolvers/history.ts b/packages/backend/server/src/core/workspaces/resolvers/history.ts index d11d2b6f03..de9ab71889 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/history.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/history.ts @@ -12,11 +12,20 @@ import { import type { SnapshotHistory } from '@prisma/client'; import { CurrentUser } from '../../auth'; -import { PgWorkspaceDocStorageAdapter } from '../../doc'; +import { type Editor, PgWorkspaceDocStorageAdapter } from '../../doc'; import { Permission, PermissionService } from '../../permission'; import { DocID } from '../../utils/doc'; import { WorkspaceType } from '../types'; +@ObjectType() +class EditorType implements Partial { + @Field() + name!: string; + + @Field(() => String, { nullable: true }) + avatarUrl!: string | null; +} + @ObjectType() class DocHistoryType implements Partial { @Field() @@ -27,6 +36,9 @@ class DocHistoryType implements Partial { @Field(() => GraphQLISODateTime) timestamp!: Date; + + @Field(() => EditorType, { nullable: true }) + editor!: EditorType | null; } @Resolver(() => WorkspaceType) @@ -47,17 +59,18 @@ export class DocHistoryResolver { ): Promise { const docId = new DocID(guid, workspace.id); - const timestamps = await this.workspace.listDocHistories( + const histories = await this.workspace.listDocHistories( workspace.id, docId.guid, { before: timestamp.getTime(), limit: take } ); - return timestamps.map(timestamp => { + return histories.map(history => { return { workspaceId: workspace.id, id: docId.guid, - timestamp: new Date(timestamp), + timestamp: new Date(history.timestamp), + editor: history.editor, }; }); } @@ -79,6 +92,7 @@ export class DocHistoryResolver { ); await this.workspace.rollbackDoc( + user.id, docId.workspace, docId.guid, timestamp.getTime() diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 625a391927..58c1bd0756 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -189,6 +189,7 @@ type DocHistoryNotFoundDataType { } type DocHistoryType { + editor: EditorType id: String! timestamp: DateTime! workspaceId: String! @@ -199,6 +200,11 @@ type DocNotFoundDataType { spaceId: String! } +type EditorType { + avatarUrl: String + name: String! +} + union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | VersionRejectedDataType enum ErrorNames { diff --git a/packages/frontend/graphql/src/graphql/histories.gql b/packages/frontend/graphql/src/graphql/histories.gql index b6aade3e0c..90eef6ab86 100644 --- a/packages/frontend/graphql/src/graphql/histories.gql +++ b/packages/frontend/graphql/src/graphql/histories.gql @@ -8,6 +8,10 @@ query listHistory( histories(guid: $pageDocId, take: $take, before: $before) { id timestamp + editor { + name + avatarUrl + } } } } diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index b78f0d4eb4..d42036c06c 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -695,6 +695,10 @@ query listHistory($workspaceId: String!, $pageDocId: String!, $take: Int, $befor histories(guid: $pageDocId, take: $take, before: $before) { id timestamp + editor { + name + avatarUrl + } } } }`, diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index 2ba9e554bc..3d4cde1c41 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -238,6 +238,7 @@ export interface DocHistoryNotFoundDataType { export interface DocHistoryType { __typename?: 'DocHistoryType'; + editor: Maybe; id: Scalars['String']['output']; timestamp: Scalars['DateTime']['output']; workspaceId: Scalars['String']['output']; @@ -249,6 +250,12 @@ export interface DocNotFoundDataType { spaceId: Scalars['String']['output']; } +export interface EditorType { + __typename?: 'EditorType'; + avatarUrl: Maybe; + name: Scalars['String']['output']; +} + export type ErrorDataUnion = | AlreadyInSpaceDataType | BlobNotFoundDataType @@ -1862,6 +1869,11 @@ export type ListHistoryQuery = { __typename?: 'DocHistoryType'; id: string; timestamp: string; + editor: { + __typename?: 'EditorType'; + name: string; + avatarUrl: string | null; + } | null; }>; }; };