mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(server): docs pagination (#12086)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added paginated document listing for workspaces, allowing users to browse documents with pagination controls. - Enhanced document details to display creation and update timestamps, as well as information about the creator and last updater. - **Bug Fixes** - Updated deprecation notice for workspace document metadata fields to guide users to the latest recommended field. - **Tests** - Added new tests to verify document info retrieval and pagination functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -658,4 +658,79 @@ test('should find metas by workspaceIds and docIds', async t => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should get doc info', async t => {
|
||||
const docId = randomUUID();
|
||||
const snapshot = {
|
||||
spaceId: workspace.id,
|
||||
docId,
|
||||
blob: Buffer.from('blob1'),
|
||||
timestamp: Date.now(),
|
||||
editorId: user.id,
|
||||
};
|
||||
|
||||
await t.context.doc.upsert(snapshot);
|
||||
await t.context.doc.upsertMeta(workspace.id, docId);
|
||||
|
||||
const docInfo = await t.context.doc.getDocInfo(workspace.id, docId);
|
||||
|
||||
t.like(docInfo, {
|
||||
workspaceId: workspace.id,
|
||||
docId,
|
||||
updatedAt: new Date(snapshot.timestamp),
|
||||
creatorId: user.id,
|
||||
lastUpdaterId: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
test('should paginate docs info', async t => {
|
||||
const docId1 = randomUUID();
|
||||
const docId2 = randomUUID();
|
||||
const docId3 = randomUUID();
|
||||
const snapshot1 = {
|
||||
spaceId: workspace.id,
|
||||
docId: docId1,
|
||||
blob: Buffer.from('blob1'),
|
||||
timestamp: Date.now(),
|
||||
editorId: user.id,
|
||||
};
|
||||
const snapshot2 = {
|
||||
spaceId: workspace.id,
|
||||
docId: docId2,
|
||||
blob: Buffer.from('blob2'),
|
||||
timestamp: Date.now() + 1,
|
||||
editorId: user.id,
|
||||
};
|
||||
const snapshot3 = {
|
||||
spaceId: workspace.id,
|
||||
docId: docId3,
|
||||
blob: Buffer.from('blob3'),
|
||||
timestamp: Date.now() + 2,
|
||||
editorId: user.id,
|
||||
};
|
||||
await t.context.doc.upsertMeta(workspace.id, docId1);
|
||||
await t.context.doc.upsertMeta(workspace.id, docId2);
|
||||
await t.context.doc.upsertMeta(workspace.id, docId3);
|
||||
await t.context.doc.upsert(snapshot1);
|
||||
await t.context.doc.upsert(snapshot2);
|
||||
await t.context.doc.upsert(snapshot3);
|
||||
|
||||
let [count, docs] = await t.context.doc.paginateDocInfo(workspace.id, {
|
||||
first: 1,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
t.is(count, 3);
|
||||
t.is(docs.length, 1);
|
||||
t.is(docs[0].docId, docId1);
|
||||
|
||||
[count, docs] = await t.context.doc.paginateDocInfo(workspace.id, {
|
||||
first: 1,
|
||||
offset: 0,
|
||||
after: docs[0].createdAt.toISOString(),
|
||||
});
|
||||
|
||||
t.is(count, 3);
|
||||
t.is(docs.length, 1);
|
||||
t.is(docs[0].docId, docId2);
|
||||
});
|
||||
// #endregion
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import type { WorkspaceDoc as PrismaWorkspaceDoc } from '@prisma/client';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import {
|
||||
@@ -37,7 +36,7 @@ import {
|
||||
DocAction,
|
||||
DocRole,
|
||||
} from '../../permission';
|
||||
import { WorkspaceUserType } from '../../user';
|
||||
import { PublicUserType, WorkspaceUserType } from '../../user';
|
||||
import { WorkspaceType } from '../types';
|
||||
import {
|
||||
DotToUnderline,
|
||||
@@ -50,7 +49,7 @@ registerEnumType(PublicDocMode, {
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
class DocType implements Partial<PrismaWorkspaceDoc> {
|
||||
class DocType {
|
||||
@Field(() => String, { name: 'id' })
|
||||
docId!: string;
|
||||
|
||||
@@ -65,6 +64,18 @@ class DocType implements Partial<PrismaWorkspaceDoc> {
|
||||
|
||||
@Field(() => DocRole)
|
||||
defaultRole!: DocRole;
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
createdAt?: Date;
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
updatedAt?: Date;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
creatorId?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
lastUpdaterId?: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
@@ -133,6 +144,9 @@ class GrantedDocUserType {
|
||||
@ObjectType()
|
||||
class PaginatedGrantedDocUserType extends Paginated(GrantedDocUserType) {}
|
||||
|
||||
@ObjectType()
|
||||
class PaginatedDocType extends Paginated(DocType) {}
|
||||
|
||||
const DocPermissions = registerObjectType<
|
||||
Record<DotToUnderline<DocAction>, boolean>
|
||||
>(
|
||||
@@ -173,6 +187,7 @@ class WorkspaceDocMeta {
|
||||
@Field(() => EditorType, { nullable: true })
|
||||
updatedBy!: EditorType | null;
|
||||
}
|
||||
|
||||
@Resolver(() => WorkspaceType)
|
||||
export class WorkspaceDocResolver {
|
||||
private readonly logger = new Logger(WorkspaceDocResolver.name);
|
||||
@@ -190,7 +205,7 @@ export class WorkspaceDocResolver {
|
||||
@ResolveField(() => WorkspaceDocMeta, {
|
||||
description: 'Cloud page metadata of workspace',
|
||||
complexity: 2,
|
||||
deprecationReason: 'use [WorkspaceType.doc.meta] instead',
|
||||
deprecationReason: 'use [WorkspaceType.doc] instead',
|
||||
})
|
||||
async pageMeta(
|
||||
@Parent() workspace: WorkspaceType,
|
||||
@@ -238,6 +253,19 @@ export class WorkspaceDocResolver {
|
||||
return this.doc(workspace, pageId);
|
||||
}
|
||||
|
||||
@ResolveField(() => PaginatedDocType)
|
||||
async docs(
|
||||
@Parent() workspace: WorkspaceType,
|
||||
@Args('pagination', PaginationInput.decode) pagination: PaginationInput
|
||||
): Promise<PaginatedDocType> {
|
||||
const [count, rows] = await this.models.doc.paginateDocInfo(
|
||||
workspace.id,
|
||||
pagination
|
||||
);
|
||||
|
||||
return paginate(rows, 'createdAt', pagination, count);
|
||||
}
|
||||
|
||||
@ResolveField(() => DocType, {
|
||||
description: 'Get get with given id',
|
||||
complexity: 2,
|
||||
@@ -246,7 +274,7 @@ export class WorkspaceDocResolver {
|
||||
@Parent() workspace: WorkspaceType,
|
||||
@Args('docId') docId: string
|
||||
): Promise<DocType> {
|
||||
const doc = await this.models.doc.getMeta(workspace.id, docId);
|
||||
const doc = await this.models.doc.getDocInfo(workspace.id, docId);
|
||||
if (doc) {
|
||||
return doc;
|
||||
}
|
||||
@@ -411,6 +439,30 @@ export class DocResolver {
|
||||
private readonly models: Models
|
||||
) {}
|
||||
|
||||
@ResolveField(() => PublicUserType, {
|
||||
nullable: true,
|
||||
description: 'Doc create user',
|
||||
})
|
||||
async createdBy(@Parent() doc: DocType): Promise<PublicUserType | null> {
|
||||
if (!doc.creatorId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.models.user.get(doc.creatorId);
|
||||
}
|
||||
|
||||
@ResolveField(() => PublicUserType, {
|
||||
nullable: true,
|
||||
description: 'Doc last updated user',
|
||||
})
|
||||
async lastUpdatedBy(@Parent() doc: DocType): Promise<PublicUserType | null> {
|
||||
if (!doc.lastUpdaterId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.models.user.get(doc.lastUpdaterId);
|
||||
}
|
||||
|
||||
@ResolveField(() => WorkspaceDocMeta, {
|
||||
description: 'Doc metadata',
|
||||
complexity: 2,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Transactional } from '@nestjs-cls/transactional';
|
||||
import type { Update } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { PaginationInput } from '../base';
|
||||
import { DocIsNotPublic } from '../base/error';
|
||||
import { BaseModel } from './base';
|
||||
import { Doc, DocRole, PublicDocMode, publicUserSelect } from './common';
|
||||
@@ -480,5 +481,92 @@ export class DocModel extends BaseModel {
|
||||
});
|
||||
return docMeta?.public ?? false;
|
||||
}
|
||||
|
||||
async getDocInfo(workspaceId: string, docId: string) {
|
||||
const rows = await this.db.$queryRaw<
|
||||
{
|
||||
workspaceId: string;
|
||||
docId: string;
|
||||
mode: PublicDocMode;
|
||||
public: boolean;
|
||||
defaultRole: DocRole;
|
||||
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",
|
||||
"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}
|
||||
AND "workspace_pages"."page_id" = ${docId}
|
||||
LIMIT 1;
|
||||
`;
|
||||
|
||||
return rows.at(0) ?? null;
|
||||
}
|
||||
|
||||
async paginateDocInfo(workspaceId: string, pagination: PaginationInput) {
|
||||
const count = await this.db.workspaceDoc.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const after = pagination.after
|
||||
? Prisma.sql`AND "snapshots"."created_at" > ${new Date(pagination.after)}`
|
||||
: Prisma.sql``;
|
||||
|
||||
const rows = await this.db.$queryRaw<
|
||||
{
|
||||
workspaceId: string;
|
||||
docId: string;
|
||||
mode: PublicDocMode;
|
||||
public: boolean;
|
||||
defaultRole: DocRole;
|
||||
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",
|
||||
"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"."created_at" ASC
|
||||
LIMIT ${pagination.first}
|
||||
OFFSET ${pagination.offset}
|
||||
`;
|
||||
|
||||
return [count, rows] as const;
|
||||
}
|
||||
// #endregion
|
||||
}
|
||||
|
||||
@@ -425,20 +425,35 @@ enum DocRole {
|
||||
}
|
||||
|
||||
type DocType {
|
||||
createdAt: DateTime
|
||||
|
||||
"""Doc create user"""
|
||||
createdBy: PublicUserType
|
||||
creatorId: String
|
||||
defaultRole: DocRole!
|
||||
|
||||
"""paginated doc granted users list"""
|
||||
grantedUsersList(pagination: PaginationInput!): PaginatedGrantedDocUserType!
|
||||
id: String!
|
||||
|
||||
"""Doc last updated user"""
|
||||
lastUpdatedBy: PublicUserType
|
||||
lastUpdaterId: String
|
||||
|
||||
"""Doc metadata"""
|
||||
meta: WorkspaceDocMeta!
|
||||
mode: PublicDocMode!
|
||||
permissions: DocPermissions!
|
||||
public: Boolean!
|
||||
updatedAt: DateTime
|
||||
workspaceId: String!
|
||||
}
|
||||
|
||||
type DocTypeEdge {
|
||||
cursor: String!
|
||||
node: DocType!
|
||||
}
|
||||
|
||||
type DocUpdateBlockedDataType {
|
||||
docId: String!
|
||||
spaceId: String!
|
||||
@@ -1189,6 +1204,12 @@ type PaginatedCopilotWorkspaceFileType {
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
type PaginatedDocType {
|
||||
edges: [DocTypeEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
type PaginatedGrantedDocUserType {
|
||||
edges: [GrantedDocUserTypeEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
@@ -1781,6 +1802,7 @@ type WorkspaceType {
|
||||
|
||||
"""Get get with given id"""
|
||||
doc(docId: String!): DocType!
|
||||
docs(pagination: PaginationInput!): PaginatedDocType!
|
||||
embedding: CopilotWorkspaceConfig!
|
||||
|
||||
"""Enable AI"""
|
||||
@@ -1817,7 +1839,7 @@ type WorkspaceType {
|
||||
owner: UserType!
|
||||
|
||||
"""Cloud page metadata of workspace"""
|
||||
pageMeta(pageId: String!): WorkspaceDocMeta! @deprecated(reason: "use [WorkspaceType.doc.meta] instead")
|
||||
pageMeta(pageId: String!): WorkspaceDocMeta! @deprecated(reason: "use [WorkspaceType.doc] instead")
|
||||
|
||||
"""map of action permissions"""
|
||||
permissions: WorkspacePermissions!
|
||||
|
||||
Reference in New Issue
Block a user