diff --git a/packages/backend/server/src/__tests__/e2e/doc/resolver.spec.ts b/packages/backend/server/src/__tests__/e2e/doc/resolver.spec.ts new file mode 100644 index 000000000..48285b5d5 --- /dev/null +++ b/packages/backend/server/src/__tests__/e2e/doc/resolver.spec.ts @@ -0,0 +1,62 @@ +import { getRecentlyUpdatedDocsQuery } from '@affine/graphql'; + +import { Mockers } from '../../mocks'; +import { app, e2e } from '../test'; + +e2e('should get recently updated docs', async t => { + const owner = await app.signup(); + + const workspace = await app.create(Mockers.Workspace, { + owner: { id: owner.id }, + }); + + const docSnapshot1 = await app.create(Mockers.DocSnapshot, { + workspaceId: workspace.id, + user: owner, + }); + const doc1 = await app.create(Mockers.DocMeta, { + workspaceId: workspace.id, + docId: docSnapshot1.id, + title: 'doc1', + }); + + const docSnapshot2 = await app.create(Mockers.DocSnapshot, { + workspaceId: workspace.id, + user: owner, + }); + const doc2 = await app.create(Mockers.DocMeta, { + workspaceId: workspace.id, + docId: docSnapshot2.id, + title: 'doc2', + }); + + const docSnapshot3 = await app.create(Mockers.DocSnapshot, { + workspaceId: workspace.id, + user: owner, + }); + const doc3 = await app.create(Mockers.DocMeta, { + workspaceId: workspace.id, + docId: docSnapshot3.id, + title: 'doc3', + }); + + const { + workspace: { recentlyUpdatedDocs }, + } = await app.gql({ + query: getRecentlyUpdatedDocsQuery, + variables: { + workspaceId: workspace.id, + pagination: { + first: 10, + }, + }, + }); + + t.is(recentlyUpdatedDocs.totalCount, 3); + t.is(recentlyUpdatedDocs.edges[0].node.id, doc3.docId); + t.is(recentlyUpdatedDocs.edges[0].node.title, doc3.title); + t.is(recentlyUpdatedDocs.edges[1].node.id, doc2.docId); + t.is(recentlyUpdatedDocs.edges[1].node.title, doc2.title); + t.is(recentlyUpdatedDocs.edges[2].node.id, doc1.docId); + t.is(recentlyUpdatedDocs.edges[2].node.title, doc1.title); +}); diff --git a/packages/backend/server/src/core/workspaces/resolvers/doc.ts b/packages/backend/server/src/core/workspaces/resolvers/doc.ts index 1e02196b6..c3fc04369 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/doc.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/doc.ts @@ -76,6 +76,9 @@ class DocType { @Field(() => String, { nullable: true }) lastUpdaterId?: string; + + @Field(() => String, { nullable: true }) + title?: string | null; } @InputType() @@ -266,6 +269,26 @@ export class WorkspaceDocResolver { return paginate(rows, 'createdAt', pagination, count); } + @ResolveField(() => PaginatedDocType, { + description: 'Get recently updated docs of a workspace', + }) + async recentlyUpdatedDocs( + @CurrentUser() me: CurrentUser, + @Parent() workspace: WorkspaceType, + @Args('pagination', PaginationInput.decode) pagination: PaginationInput + ): Promise { + const [count, rows] = await this.models.doc.paginateDocInfoByUpdatedAt( + workspace.id, + pagination + ); + const needs = await this.ac + .user(me.id) + .workspace(workspace.id) + .docs(rows, 'Doc.Read'); + + return paginate(needs, 'updatedAt', pagination, count); + } + @ResolveField(() => DocType, { description: 'Get get with given id', complexity: 2, diff --git a/packages/backend/server/src/models/doc.ts b/packages/backend/server/src/models/doc.ts index 0f56fc3fd..8e611da29 100644 --- a/packages/backend/server/src/models/doc.ts +++ b/packages/backend/server/src/models/doc.ts @@ -636,5 +636,61 @@ export class DocModel extends BaseModel { return [count, rows] as const; } + + async paginateDocInfoByUpdatedAt( + workspaceId: string, + pagination: PaginationInput + ) { + const count = await this.db.workspaceDoc.count({ + where: { + workspaceId, + }, + }); + + const after = pagination.after + ? Prisma.sql`AND "snapshots"."updated_at" < ${new Date(pagination.after)}` + : Prisma.sql``; + + const rows = await this.db.$queryRaw< + { + workspaceId: string; + docId: string; + mode: PublicDocMode; + public: boolean; + defaultRole: DocRole; + title: string | null; + createdAt: Date; + updatedAt: Date; + creatorId?: string; + lastUpdaterId?: string; + }[] + >` + SELECT + "workspace_pages"."workspace_id" as "workspaceId", + "workspace_pages"."page_id" as "docId", + "workspace_pages"."mode" as "mode", + "workspace_pages"."public" as "public", + "workspace_pages"."defaultRole" as "defaultRole", + "workspace_pages"."title" as "title", + "snapshots"."created_at" as "createdAt", + "snapshots"."updated_at" as "updatedAt", + "snapshots"."created_by" as "creatorId", + "snapshots"."updated_by" as "lastUpdaterId" + FROM "workspace_pages" + INNER JOIN "snapshots" + ON "workspace_pages"."workspace_id" = "snapshots"."workspace_id" + AND "workspace_pages"."page_id" = "snapshots"."guid" + WHERE + "workspace_pages"."workspace_id" = ${workspaceId} + ${after} + ORDER BY + "snapshots"."updated_at" DESC + LIMIT ${pagination.first} + OFFSET ${pagination.offset} + `; + + return [count, rows] as const; + } + // #endregion } diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 2567ba678..6fc204c77 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -517,6 +517,7 @@ type DocType { mode: PublicDocMode! permissions: DocPermissions! public: Boolean! + title: String updatedAt: DateTime workspaceId: String! } @@ -2062,6 +2063,9 @@ type WorkspaceType { """quota of workspace""" quota: WorkspaceQuotaType! + """Get recently updated docs of a workspace""" + recentlyUpdatedDocs(pagination: PaginationInput!): PaginatedDocType! + """Role of current signed in user in workspace""" role: Permission! diff --git a/packages/common/graphql/src/graphql/get-recently-update-docs.gql b/packages/common/graphql/src/graphql/get-recently-update-docs.gql new file mode 100644 index 000000000..4355d0aa6 --- /dev/null +++ b/packages/common/graphql/src/graphql/get-recently-update-docs.gql @@ -0,0 +1,21 @@ +query getRecentlyUpdatedDocs($workspaceId: String!, $pagination: PaginationInput!) { + workspace(id: $workspaceId) { + recentlyUpdatedDocs(pagination: $pagination) { + totalCount + pageInfo { + endCursor + hasNextPage + } + edges { + node { + id + title + createdAt + updatedAt + creatorId + lastUpdaterId + } + } + } + } +} diff --git a/packages/common/graphql/src/graphql/index.ts b/packages/common/graphql/src/graphql/index.ts index a600bb02a..24a49075b 100644 --- a/packages/common/graphql/src/graphql/index.ts +++ b/packages/common/graphql/src/graphql/index.ts @@ -1180,6 +1180,32 @@ export const getPublicUserByIdQuery = { }`, }; +export const getRecentlyUpdatedDocsQuery = { + id: 'getRecentlyUpdatedDocsQuery' as const, + op: 'getRecentlyUpdatedDocs', + query: `query getRecentlyUpdatedDocs($workspaceId: String!, $pagination: PaginationInput!) { + workspace(id: $workspaceId) { + recentlyUpdatedDocs(pagination: $pagination) { + totalCount + pageInfo { + endCursor + hasNextPage + } + edges { + node { + id + title + createdAt + updatedAt + creatorId + lastUpdaterId + } + } + } + } +}`, +}; + export const getUserFeaturesQuery = { id: 'getUserFeaturesQuery' as const, op: 'getUserFeatures', diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index 138425eea..4f635460d 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -630,6 +630,7 @@ export interface DocType { mode: PublicDocMode; permissions: DocPermissions; public: Scalars['Boolean']['output']; + title: Maybe; updatedAt: Maybe; workspaceId: Scalars['String']['output']; } @@ -2647,6 +2648,8 @@ export interface WorkspaceType { publicPages: Array; /** quota of workspace */ quota: WorkspaceQuotaType; + /** Get recently updated docs of a workspace */ + recentlyUpdatedDocs: PaginatedDocType; /** Role of current signed in user in workspace */ role: Permission; /** Search a specific table */ @@ -2694,6 +2697,10 @@ export interface WorkspaceTypePublicPageArgs { pageId: Scalars['String']['input']; } +export interface WorkspaceTypeRecentlyUpdatedDocsArgs { + pagination: PaginationInput; +} + export interface WorkspaceTypeSearchArgs { input: SearchInput; } @@ -4064,6 +4071,39 @@ export type GetPublicUserByIdQuery = { } | null; }; +export type GetRecentlyUpdatedDocsQueryVariables = Exact<{ + workspaceId: Scalars['String']['input']; + pagination: PaginationInput; +}>; + +export type GetRecentlyUpdatedDocsQuery = { + __typename?: 'Query'; + workspace: { + __typename?: 'WorkspaceType'; + recentlyUpdatedDocs: { + __typename?: 'PaginatedDocType'; + totalCount: number; + pageInfo: { + __typename?: 'PageInfo'; + endCursor: string | null; + hasNextPage: boolean; + }; + edges: Array<{ + __typename?: 'DocTypeEdge'; + node: { + __typename?: 'DocType'; + id: string; + title: string | null; + createdAt: string | null; + updatedAt: string | null; + creatorId: string | null; + lastUpdaterId: string | null; + }; + }>; + }; + }; +}; + export type GetUserFeaturesQueryVariables = Exact<{ [key: string]: never }>; export type GetUserFeaturesQuery = { @@ -5197,6 +5237,11 @@ export type Queries = variables: GetPublicUserByIdQueryVariables; response: GetPublicUserByIdQuery; } + | { + name: 'getRecentlyUpdatedDocsQuery'; + variables: GetRecentlyUpdatedDocsQueryVariables; + response: GetRecentlyUpdatedDocsQuery; + } | { name: 'getUserFeaturesQuery'; variables: GetUserFeaturesQueryVariables;