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 {
id String @id @default(uuid()) @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
pageId String @map("page_id") @db.VarChar
userId String @map("user_id") @db.VarChar
// External/Reader/Editor/Manager/Owner
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)
user User @relation(fields: [userId], 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")
}

View File

@@ -84,3 +84,6 @@ export type GraphqlContext = {
],
})
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';
export * from './error';
export { EventBus, OnEvent } from './event';
export type { GraphqlContext } from './graphql';
export { registerObjectType } from './graphql/register';
export {
type GraphqlContext,
paginate,
Paginated,
PaginationInput,
registerObjectType,
} from './graphql';
export * from './guard';
export { CryptoHelper, URLHelper } from './helpers';
export { AFFiNELogger } from './logger';

View File

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

View File

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

View File

@@ -127,8 +127,6 @@ export class PageModel extends BaseModel {
pageId,
userId,
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!
}
type GrantedDocUserEdge {
cursor: String!
user: GrantedDocUserType!
}
type GrantedDocUserType {
pageId: String!
role: DocRole!
user: UserType!
userId: String!
workspaceId: String!
}
type GrantedDocUsersConnection {
edges: [GrantedDocUserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
type GrantedDocUserTypeEdge {
cursor: String!
node: GrantedDocUserType!
}
type InvalidEmailDataType {
@@ -685,16 +681,6 @@ enum OAuthProviderType {
OIDC
}
input PageGrantedUsersInput {
"""Cursor"""
after: String
"""Cursor"""
before: String
first: Int!
offset: Int!
}
type PageInfo {
endCursor: String
hasNextPage: Boolean!
@@ -702,6 +688,28 @@ type PageInfo {
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 {
maxLength: Int!
minLength: Int!
@@ -1200,7 +1208,7 @@ type WorkspaceType {
owner: UserType!
"""Page granted users list"""
pageGrantedUsersList(pageGrantedUsersInput: PageGrantedUsersInput!, pageId: String!): GrantedDocUsersConnection!
pageGrantedUsersList(pageId: String!, pagination: PaginationInput!): PaginatedGrantedDocUserType!
"""Cloud page metadata of workspace"""
pageMeta(pageId: String!): WorkspacePageMeta!