feat(core): integrate realtime features (#15003)

This commit is contained in:
DarkSky
2026-05-20 05:48:03 +08:00
committed by GitHub
parent 3e42bbf4fa
commit 9f33d37add
108 changed files with 3069 additions and 2729 deletions
@@ -5,6 +5,17 @@ import { BaseModel } from './base';
const REDACTED_TOKEN = '[REDACTED]';
declare global {
interface Events {
'user.access_token.created': {
userId: string;
};
'user.access_token.revoked': {
userId: string;
};
}
}
export interface CreateAccessTokenInput {
userId: string;
name: string;
@@ -10,6 +10,20 @@ import { BaseModel } from './base';
import { DocRole } from './common';
import { docRoleFromNew } from './permission-write';
declare global {
interface Events {
'doc.grants.changed': {
workspaceId: string;
docId: string;
};
'doc.owner.changed': {
workspaceId: string;
docId: string;
userId: string;
};
}
}
@Injectable()
export class DocUserModel extends BaseModel {
/**
@@ -124,6 +138,14 @@ export class DocUserModel extends BaseModel {
return grants.map(grant => this.docGrantToCompat(grant));
}
async findDirectGrantDocIdsByUser(userId: string) {
return await this.db.docGrant.findMany({
where: { principalType: 'user', principalId: userId },
select: { workspaceId: true, docId: true },
distinct: ['workspaceId', 'docId'],
});
}
count(workspaceId: string, docId: string) {
return this.db.docGrant.count({
where: {
@@ -19,6 +19,14 @@ declare global {
workspaceId: string;
docId: string;
};
'doc.public_state.changed': {
workspaceId: string;
docId: string;
};
'doc.default_role.changed': {
workspaceId: string;
docId: string;
};
}
}
@@ -4,6 +4,14 @@ import z from 'zod';
import { BaseModel } from './base';
declare global {
interface Events {
'user.settings.updated': {
userId: string;
};
}
}
export const UserSettingsSchema = z.object({
receiveInvitationEmail: z.boolean().default(true),
receiveMentionEmail: z.boolean().default(true),
+1 -1
View File
@@ -221,7 +221,7 @@ export class UserModel extends BaseModel {
});
this.logger.debug(`User [${user.id}] updated`);
this.event.emit('user.updated', user);
this.event.emitDetached('user.updated', user);
return user;
}
@@ -186,36 +186,68 @@ export async function searchCompatRows(
pagination: { first: number; offset: number; after?: string | Date }
) {
const after = pagination.after
? Prisma.sql`AND wm.created_at >= ${pagination.after}::timestamptz`
? Prisma.sql`AND created_at >= ${pagination.after}::timestamptz`
: Prisma.empty;
const rows = await db.$queryRaw<WorkspaceUserCompatRow[]>`
SELECT
COALESCE(wm.legacy_permission_id, wm.id) AS id,
wm.workspace_id AS "workspaceId",
wm.user_id AS "userId",
CASE wm.role
WHEN 'owner' THEN ${WorkspaceRole.Owner}
WHEN 'admin' THEN ${WorkspaceRole.Admin}
ELSE ${WorkspaceRole.Collaborator}
END AS type,
'Accepted'::"WorkspaceMemberStatus" AS status,
CASE wm.source
WHEN 'link' THEN 'Link'::"WorkspaceMemberSource"
ELSE 'Email'::"WorkspaceMemberSource"
END AS source,
NULL::varchar AS "inviterId",
wm.created_at AS "createdAt",
wm.updated_at AS "updatedAt",
u.name AS "userName",
u.email AS "userEmail",
u.avatar_url AS "userAvatarUrl"
FROM workspace_members wm
INNER JOIN users u ON u.id = wm.user_id
WHERE wm.workspace_id = ${workspaceId}
AND wm.state = 'active'
AND (u.email ILIKE ${`%${query}%`} OR u.name ILIKE ${`%${query}%`})
SELECT *
FROM (
SELECT
COALESCE(wm.legacy_permission_id, wm.id) AS id,
wm.workspace_id AS "workspaceId",
wm.user_id AS "userId",
CASE wm.role
WHEN 'owner' THEN ${WorkspaceRole.Owner}
WHEN 'admin' THEN ${WorkspaceRole.Admin}
ELSE ${WorkspaceRole.Collaborator}
END AS type,
'Accepted'::"WorkspaceMemberStatus" AS status,
CASE wm.source
WHEN 'link' THEN 'Link'::"WorkspaceMemberSource"
ELSE 'Email'::"WorkspaceMemberSource"
END AS source,
NULL::varchar AS "inviterId",
wm.created_at AS "createdAt",
wm.updated_at AS "updatedAt",
u.name AS "userName",
u.email AS "userEmail",
u.avatar_url AS "userAvatarUrl",
wm.created_at AS created_at
FROM workspace_members wm
INNER JOIN users u ON u.id = wm.user_id
WHERE wm.workspace_id = ${workspaceId}
AND wm.state = 'active'
UNION ALL
SELECT
COALESCE(wi.legacy_permission_id, wi.id) AS id,
wi.workspace_id AS "workspaceId",
wi.invitee_user_id AS "userId",
CASE wi.requested_role
WHEN 'admin' THEN ${WorkspaceRole.Admin}
ELSE ${WorkspaceRole.Collaborator}
END AS type,
CASE wi.status
WHEN 'waiting_review' THEN 'UnderReview'::"WorkspaceMemberStatus"
WHEN 'waiting_seat' THEN 'NeedMoreSeat'::"WorkspaceMemberStatus"
ELSE 'Pending'::"WorkspaceMemberStatus"
END AS status,
CASE wi.kind
WHEN 'link' THEN 'Link'::"WorkspaceMemberSource"
ELSE 'Email'::"WorkspaceMemberSource"
END AS source,
wi.inviter_user_id AS "inviterId",
wi.created_at AS "createdAt",
wi.updated_at AS "updatedAt",
u.name AS "userName",
u.email AS "userEmail",
u.avatar_url AS "userAvatarUrl",
wi.created_at AS created_at
FROM workspace_invitations wi
INNER JOIN users u ON u.id = wi.invitee_user_id
WHERE wi.workspace_id = ${workspaceId}
) roles
WHERE ("userEmail" ILIKE ${`%${query}%`} OR "userName" ILIKE ${`%${query}%`})
${after}
ORDER BY wm.created_at ASC
ORDER BY created_at ASC
OFFSET ${pagination.offset}
LIMIT ${pagination.first}
`;
@@ -422,6 +422,26 @@ export class WorkspaceUserModel extends BaseModel {
return await findUserActiveWorkspaceRoles(this.db, userId, filter);
}
async getUserWorkspaceIds(userId: string) {
const [roles, invitations] = await Promise.all([
this.db.workspaceMember.findMany({
where: { userId, state: 'active' },
select: { workspaceId: true },
}),
this.db.workspaceInvitation.findMany({
where: { inviteeUserId: userId },
select: { workspaceId: true },
}),
]);
return Array.from(
new Set([
...roles.map(role => role.workspaceId),
...invitations.map(invitation => invitation.workspaceId),
])
);
}
async hasSharedWorkspace(userId: string, otherUserId: string) {
return await hasSharedWorkspace(this.db, userId, otherUserId);
}