refactor(server): remove never used column in page permission (#9985)

This commit is contained in:
forehalo
2025-02-06 10:52:05 +00:00
parent d7da12597a
commit 7c7febd495
9 changed files with 251 additions and 190 deletions

View File

@@ -0,0 +1,16 @@
/*
Warnings:
- The primary key for the `workspace_page_user_permissions` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `accepted` on the `workspace_page_user_permissions` table. All the data in the column will be lost.
- You are about to drop the column `id` on the `workspace_page_user_permissions` table. All the data in the column will be lost.
*/
-- DropIndex
DROP INDEX "workspace_page_user_permissions_workspace_id_page_id_user_i_key";
-- AlterTable
ALTER TABLE "workspace_page_user_permissions" DROP CONSTRAINT "workspace_page_user_permissions_pkey",
DROP COLUMN "accepted",
DROP COLUMN "id",
ADD CONSTRAINT "workspace_page_user_permissions_pkey" PRIMARY KEY ("workspace_id", "page_id", "user_id");

View File

@@ -161,20 +161,17 @@ model WorkspaceUserPermission {
} }
model WorkspacePageUserPermission { model WorkspacePageUserPermission {
id String @id @default(uuid()) @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar workspaceId String @map("workspace_id") @db.VarChar
pageId String @map("page_id") @db.VarChar pageId String @map("page_id") @db.VarChar
userId String @map("user_id") @db.VarChar userId String @map("user_id") @db.VarChar
// External/Reader/Editor/Manager/Owner // External/Reader/Editor/Manager/Owner
type Int @db.SmallInt type Int @db.SmallInt
/// Whether the permission invitation is accepted by the user
accepted Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@unique([workspaceId, pageId, userId]) @@id([workspaceId, pageId, userId])
@@map("workspace_page_user_permissions") @@map("workspace_page_user_permissions")
} }

View File

@@ -84,3 +84,6 @@ export type GraphqlContext = {
], ],
}) })
export class GqlModule {} export class GqlModule {}
export * from './pagination';
export { registerObjectType } from './register';

View File

@@ -0,0 +1,127 @@
import { Type } from '@nestjs/common';
import {
Field,
FieldMiddleware,
InputType,
Int,
MiddlewareContext,
NextFn,
ObjectType,
} from '@nestjs/graphql';
const parseCursorMiddleware: FieldMiddleware = async (
_ctx: MiddlewareContext,
next: NextFn
) => {
const value = await next();
return value === undefined || value === null ? null : decode(value);
};
@InputType()
export class PaginationInput {
@Field(() => Int, {
nullable: true,
description: 'returns the first n elements from the list.',
defaultValue: 10,
})
first!: number;
@Field(() => Int, {
nullable: true,
description: 'ignore the first n elements from the list.',
defaultValue: 0,
})
offset!: number;
@Field(() => String, {
nullable: true,
description:
'returns the elements in the list that come after the specified cursor.',
middleware: [parseCursorMiddleware],
})
after!: string | null;
@Field(() => String, {
nullable: true,
description:
'returns the elements in the list that come before the specified cursor.',
middleware: [parseCursorMiddleware],
})
before!: string | null;
}
const encode = (input: string) => Buffer.from(input).toString('base64');
const decode = (base64String: string) =>
Buffer.from(base64String, 'base64').toString('utf-8');
export function paginate<T>(
list: T[],
cursorField: keyof T,
paginationInput: PaginationInput,
total: number
): PaginatedType<T> {
const edges = list.map(item => ({
node: item,
cursor: encode(String(item[cursorField])),
}));
return {
totalCount: total,
edges,
pageInfo: {
hasNextPage: edges.length >= paginationInput.first,
hasPreviousPage: paginationInput.offset > 0,
endCursor: edges.length ? edges[edges.length - 1].cursor : null,
startCursor: edges.length ? edges[0].cursor : null,
},
};
}
export interface PaginatedType<T> {
totalCount: number;
edges: {
cursor: string;
node: T;
}[];
pageInfo: PageInfo;
}
@ObjectType()
export class PageInfo {
@Field(() => String, { nullable: true })
startCursor?: string | null;
@Field(() => String, { nullable: true })
endCursor?: string | null;
@Field()
hasNextPage!: boolean;
@Field()
hasPreviousPage!: boolean;
}
export function Paginated<T>(classRef: Type<T>): any {
@ObjectType(`${classRef.name}Edge`)
abstract class EdgeType {
@Field(() => String)
cursor!: string;
@Field(() => classRef)
node!: T;
}
@ObjectType({ isAbstract: true })
abstract class PaginatedType {
@Field(() => Int)
totalCount!: number;
@Field(() => [EdgeType])
edges!: EdgeType[];
@Field(() => PageInfo)
pageInfo!: PageInfo;
}
return PaginatedType;
}

View File

@@ -15,8 +15,13 @@ export {
} from './config'; } from './config';
export * from './error'; export * from './error';
export { EventBus, OnEvent } from './event'; export { EventBus, OnEvent } from './event';
export type { GraphqlContext } from './graphql'; export {
export { registerObjectType } from './graphql/register'; type GraphqlContext,
paginate,
Paginated,
PaginationInput,
registerObjectType,
} from './graphql';
export * from './guard'; export * from './guard';
export { CryptoHelper, URLHelper } from './helpers'; export { CryptoHelper, URLHelper } from './helpers';
export { AFFiNELogger } from './logger'; export { AFFiNELogger } from './logger';

View File

@@ -35,8 +35,8 @@ export class PermissionService {
const { workspaceId, docId, editor } = payload; const { workspaceId, docId, editor } = payload;
await this.prisma.$queryRaw` await this.prisma.$queryRaw`
INSERT INTO "workspace_page_user_permissions" ("workspace_id", "page_id", "user_id", "type", "accepted", "created_at", "updated_at") INSERT INTO "workspace_page_user_permissions" ("workspace_id", "page_id", "user_id", "type", "created_at")
VALUES (${workspaceId}, ${docId}, ${editor}, ${DocRole.Owner}, true, now(), now()) VALUES (${workspaceId}, ${docId}, ${editor}, ${DocRole.Owner}, now())
ON CONFLICT ("workspace_id", "page_id", "user_id") ON CONFLICT ("workspace_id", "page_id", "user_id")
DO NOTHING DO NOTHING
`; `;
@@ -576,7 +576,6 @@ export class PermissionService {
workspaceId: ws, workspaceId: ws,
pageId: page, pageId: page,
userId: user, userId: user,
accepted: true,
type: { type: {
gte: role, gte: role,
}, },
@@ -658,65 +657,48 @@ export class PermissionService {
}); });
} }
async grantPage( async grantPage(ws: string, page: string, user: string, permission: DocRole) {
ws: string, const [p] = await this.prisma.$transaction(
page: string, [
user: string, this.prisma.workspacePageUserPermission.upsert({
permission: DocRole where: {
): Promise<string> { workspaceId_pageId_userId: {
const data = await this.prisma.workspacePageUserPermission.findFirst({ workspaceId: ws,
where: { pageId: page,
workspaceId: ws, userId: user,
pageId: page,
userId: user,
accepted: true,
},
});
if (data) {
const [p] = await this.prisma.$transaction(
[
this.prisma.workspacePageUserPermission.update({
where: {
id: data.id,
}, },
data: { },
type: permission, update: {
}, type: permission,
}), },
create: {
workspaceId: ws,
pageId: page,
userId: user,
type: permission,
},
}),
// If the new permission is owner, we need to revoke old owner // If the new permission is owner, we need to revoke old owner
permission === DocRole.Owner permission === DocRole.Owner
? this.prisma.workspacePageUserPermission.updateMany({ ? this.prisma.workspacePageUserPermission.updateMany({
where: { where: {
workspaceId: ws, workspaceId: ws,
pageId: page, pageId: page,
type: DocRole.Owner, type: DocRole.Owner,
userId: { userId: {
not: user, not: user,
},
}, },
data: { },
type: DocRole.Manager, data: {
}, type: DocRole.Manager,
}) },
: null, })
].filter(Boolean) as Prisma.PrismaPromise<any>[] : null,
); ].filter(Boolean) as Prisma.PrismaPromise<any>[]
);
return p.id; return p;
}
return this.prisma.workspacePageUserPermission
.create({
data: {
workspaceId: ws,
pageId: page,
userId: user,
type: permission,
},
})
.then(p => p.id);
} }
async revokePage(ws: string, page: string, users: string[]) { async revokePage(ws: string, page: string, users: string[]) {
@@ -746,14 +728,11 @@ export class PermissionService {
if (userIds.length === 0) { if (userIds.length === 0) {
return []; return [];
} }
if (role === DocRole.Owner) { if (role === DocRole.Owner && userIds.length > 1) {
if (userIds.length > 1) { throw new SpaceShouldHaveOnlyOneOwner({ spaceId: workspaceId });
throw new SpaceShouldHaveOnlyOneOwner({ spaceId: workspaceId });
}
return [await this.grantPage(workspaceId, pageId, userIds[0], role)];
} }
const ret = await this.prisma.$transaction(async tx => return await this.prisma.$transaction(async tx =>
Promise.all( Promise.all(
userIds.map(id => userIds.map(id =>
tx.workspacePageUserPermission.upsert({ tx.workspacePageUserPermission.upsert({
@@ -777,7 +756,6 @@ export class PermissionService {
) )
) )
); );
return ret.map(p => p.id);
} }
async updatePagePermission( async updatePagePermission(
@@ -798,14 +776,17 @@ export class PermissionService {
return this.grantPage(workspaceId, pageId, userId, role); return this.grantPage(workspaceId, pageId, userId, role);
} }
const { id } = await this.prisma.workspacePageUserPermission.update({ return await this.prisma.workspacePageUserPermission.update({
where: { where: {
id: permission.id, workspaceId_pageId_userId: {
workspaceId,
pageId,
userId,
},
}, },
data: { data: {
type: role, type: role,
}, },
}); });
return id;
} }
} }

View File

@@ -3,7 +3,6 @@ import {
Args, Args,
Field, Field,
InputType, InputType,
Int,
Mutation, Mutation,
ObjectType, ObjectType,
Parent, Parent,
@@ -21,6 +20,9 @@ import {
ExpectToRevokePublicPage, ExpectToRevokePublicPage,
ExpectToUpdateDocUserRole, ExpectToUpdateDocUserRole,
PageIsNotPublic, PageIsNotPublic,
paginate,
Paginated,
PaginationInput,
registerObjectType, registerObjectType,
} from '../../../base'; } from '../../../base';
import { CurrentUser } from '../../auth'; import { CurrentUser } from '../../auth';
@@ -34,7 +36,6 @@ import {
PublicPageMode, PublicPageMode,
WorkspaceRole, WorkspaceRole,
} from '../../permission'; } from '../../permission';
import { UserType } from '../../user';
import { DocID } from '../../utils/doc'; import { DocID } from '../../utils/doc';
import { WorkspaceType } from '../types'; import { WorkspaceType } from '../types';
@@ -73,65 +74,23 @@ class GrantDocUserRolesInput {
userIds!: string[]; userIds!: string[];
} }
@InputType()
class PageGrantedUsersInput {
@Field(() => Int)
first!: number;
@Field(() => Int)
offset?: number;
@Field(() => String, { description: 'Cursor', nullable: true })
after?: string;
@Field(() => String, { description: 'Cursor', nullable: true })
before?: string;
}
@ObjectType() @ObjectType()
class GrantedDocUserType { class GrantedDocUserType {
@Field(() => UserType) @Field(() => String)
user!: UserType; workspaceId!: string;
@Field(() => DocRole)
role!: DocRole;
}
@ObjectType()
class PageInfo {
@Field(() => String, { nullable: true })
startCursor?: string;
@Field(() => String, { nullable: true })
endCursor?: string;
@Field(() => Boolean)
hasNextPage!: boolean;
@Field(() => Boolean)
hasPreviousPage!: boolean;
}
@ObjectType()
class GrantedDocUserEdge {
@Field(() => GrantedDocUserType)
user!: GrantedDocUserType;
@Field(() => String) @Field(() => String)
cursor!: string; pageId!: string;
@Field(() => String)
userId!: string;
@Field(() => DocRole, { name: 'role' })
type!: DocRole;
} }
@ObjectType() @ObjectType()
class GrantedDocUsersConnection { class PaginatedGrantedDocUserType extends Paginated(GrantedDocUserType) {}
@Field(() => Int)
totalCount!: number;
@Field(() => [GrantedDocUserEdge])
edges!: GrantedDocUserEdge[];
@Field(() => PageInfo)
pageInfo!: PageInfo;
}
const DocPermissions = registerObjectType<DocActionPermissions>( const DocPermissions = registerObjectType<DocActionPermissions>(
Object.fromEntries( Object.fromEntries(
@@ -261,16 +220,15 @@ export class PagePermissionResolver {
}; };
} }
@ResolveField(() => GrantedDocUsersConnection, { @ResolveField(() => PaginatedGrantedDocUserType, {
description: 'Page granted users list', description: 'Page granted users list',
complexity: 4, complexity: 4,
}) })
async pageGrantedUsersList( async pageGrantedUsersList(
@Parent() workspace: WorkspaceType, @Parent() workspace: WorkspaceType,
@Args('pageId') pageId: string, @Args('pageId') pageId: string,
@Args('pageGrantedUsersInput') @Args('pagination') pagination: PaginationInput
pageGrantedUsersInput: PageGrantedUsersInput ): Promise<PaginatedGrantedDocUserType> {
): Promise<GrantedDocUsersConnection> {
const docId = new DocID(pageId, workspace.id); const docId = new DocID(pageId, workspace.id);
const [permissions, totalCount] = await this.prisma.$transaction(tx => { const [permissions, totalCount] = await this.prisma.$transaction(tx => {
return Promise.all([ return Promise.all([
@@ -279,19 +237,11 @@ export class PagePermissionResolver {
workspaceId: workspace.id, workspaceId: workspace.id,
pageId: docId.guid, pageId: docId.guid,
}, },
include: {
user: true,
},
orderBy: { orderBy: {
createdAt: 'desc', createdAt: 'desc',
}, },
take: pageGrantedUsersInput.first, take: pagination.first,
skip: pageGrantedUsersInput.offset, skip: pagination.offset,
cursor: pageGrantedUsersInput.after
? {
id: pageGrantedUsersInput.after,
}
: undefined,
}), }),
tx.workspacePageUserPermission.count({ tx.workspacePageUserPermission.count({
where: { where: {
@@ -302,31 +252,7 @@ export class PagePermissionResolver {
]); ]);
}); });
return { return paginate(permissions, 'createdAt', pagination, totalCount);
totalCount,
edges: permissions.map(permission => ({
user: {
user: {
id: permission.user.id,
name: permission.user.name,
email: permission.user.email,
avatarUrl: permission.user.avatarUrl,
emailVerified: permission.user.emailVerifiedAt !== null,
hasPassword: permission.user.password !== null,
},
role: permission.type,
},
cursor: permission.id,
})),
pageInfo: {
startCursor: permissions.at(0)?.id,
endCursor: permissions.at(-1)?.id,
hasNextPage: totalCount > pageGrantedUsersInput.first,
hasPreviousPage:
pageGrantedUsersInput.offset !== undefined &&
pageGrantedUsersInput.offset > 0,
},
};
} }
/** /**

View File

@@ -127,8 +127,6 @@ export class PageModel extends BaseModel {
pageId, pageId,
userId, userId,
type: permission, type: permission,
// page permission does not require invitee to accept, the accepted field will be deprecated later.
accepted: true,
}, },
}); });
} }

View File

@@ -383,20 +383,16 @@ input GrantDocUserRolesInput {
workspaceId: String! workspaceId: String!
} }
type GrantedDocUserEdge {
cursor: String!
user: GrantedDocUserType!
}
type GrantedDocUserType { type GrantedDocUserType {
pageId: String!
role: DocRole! role: DocRole!
user: UserType! userId: String!
workspaceId: String!
} }
type GrantedDocUsersConnection { type GrantedDocUserTypeEdge {
edges: [GrantedDocUserEdge!]! cursor: String!
pageInfo: PageInfo! node: GrantedDocUserType!
totalCount: Int!
} }
type InvalidEmailDataType { type InvalidEmailDataType {
@@ -685,16 +681,6 @@ enum OAuthProviderType {
OIDC OIDC
} }
input PageGrantedUsersInput {
"""Cursor"""
after: String
"""Cursor"""
before: String
first: Int!
offset: Int!
}
type PageInfo { type PageInfo {
endCursor: String endCursor: String
hasNextPage: Boolean! hasNextPage: Boolean!
@@ -702,6 +688,28 @@ type PageInfo {
startCursor: String startCursor: String
} }
type PaginatedGrantedDocUserType {
edges: [GrantedDocUserTypeEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
input PaginationInput {
"""returns the elements in the list that come after the specified cursor."""
after: String
"""
returns the elements in the list that come before the specified cursor.
"""
before: String
"""returns the first n elements from the list."""
first: Int = 10
"""ignore the first n elements from the list."""
offset: Int = 0
}
type PasswordLimitsType { type PasswordLimitsType {
maxLength: Int! maxLength: Int!
minLength: Int! minLength: Int!
@@ -1200,7 +1208,7 @@ type WorkspaceType {
owner: UserType! owner: UserType!
"""Page granted users list""" """Page granted users list"""
pageGrantedUsersList(pageGrantedUsersInput: PageGrantedUsersInput!, pageId: String!): GrantedDocUsersConnection! pageGrantedUsersList(pageId: String!, pagination: PaginationInput!): PaginatedGrantedDocUserType!
"""Cloud page metadata of workspace""" """Cloud page metadata of workspace"""
pageMeta(pageId: String!): WorkspacePageMeta! pageMeta(pageId: String!): WorkspacePageMeta!