feat(server): get recently updated docs (#12861)

close AI-218



#### PR Dependency Tree


* **PR #12861** 👈

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**
- Added a "Recently Updated Documents" feature, allowing users to view a
paginated list of the most recently updated documents within a
workspace.
- Document metadata now includes a "title" field for easier
identification.
- **Tests**
- Introduced new end-to-end tests to verify the recently updated
documents query and its pagination behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fengmk2
2025-06-20 11:35:39 +08:00
committed by GitHub
parent 5623d808bf
commit ad5722f637
7 changed files with 237 additions and 0 deletions

View File

@@ -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);
});

View File

@@ -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<PaginatedDocType> {
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,

View File

@@ -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
}

View File

@@ -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!