updates and fixes

* seo friendly urls
* custom client serve-static module
* database fixes
* fix recent pages
* other fixes
This commit is contained in:
Philipinho
2024-05-18 03:19:42 +01:00
parent eefe63d1cd
commit 9c7c2f1163
102 changed files with 921 additions and 536 deletions

View File

@ -0,0 +1,187 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
import { dbOrTx } from '@docmost/db/utils';
import {
InsertableSpaceMember,
SpaceMember,
UpdatableSpaceMember,
} from '@docmost/db/types/entity.types';
import { PaginationOptions } from '../../pagination/pagination-options';
import { MemberInfo, UserSpaceRole } from './types';
import { executeWithPagination } from '@docmost/db/pagination/pagination';
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
@Injectable()
export class SpaceMemberRepo {
constructor(
@InjectKysely() private readonly db: KyselyDB,
private readonly groupRepo: GroupRepo,
) {}
async insertSpaceMember(
insertableSpaceMember: InsertableSpaceMember,
trx?: KyselyTransaction,
): Promise<void> {
const db = dbOrTx(this.db, trx);
await db
.insertInto('spaceMembers')
.values(insertableSpaceMember)
.returningAll()
.execute();
}
async updateSpaceMember(
updatableSpaceMember: UpdatableSpaceMember,
spaceMemberId: string,
spaceId: string,
): Promise<void> {
await this.db
.updateTable('spaceMembers')
.set(updatableSpaceMember)
.where('id', '=', spaceMemberId)
.where('spaceId', '=', spaceId)
.execute();
}
async getSpaceMemberByTypeId(
spaceId: string,
opts: {
userId?: string;
groupId?: string;
},
trx?: KyselyTransaction,
): Promise<SpaceMember> {
const db = dbOrTx(this.db, trx);
let query = db
.selectFrom('spaceMembers')
.selectAll()
.where('spaceId', '=', spaceId);
if (opts.userId) {
query = query.where('userId', '=', opts.userId);
} else if (opts.groupId) {
query = query.where('groupId', '=', opts.groupId);
} else {
throw new BadRequestException('Please provider a userId or groupId');
}
return query.executeTakeFirst();
}
async removeSpaceMemberById(
memberId: string,
spaceId: string,
trx?: KyselyTransaction,
): Promise<void> {
const db = dbOrTx(this.db, trx);
await db
.deleteFrom('spaceMembers')
.where('id', '=', memberId)
.where('spaceId', '=', spaceId)
.execute();
}
async roleCountBySpaceId(role: string, spaceId: string): Promise<number> {
const { count } = await this.db
.selectFrom('spaceMembers')
.select((eb) => eb.fn.count('role').as('count'))
.where('role', '=', role)
.where('spaceId', '=', spaceId)
.executeTakeFirst();
return count as number;
}
async getSpaceMembersPaginated(
spaceId: string,
pagination: PaginationOptions,
) {
const query = this.db
.selectFrom('spaceMembers')
.leftJoin('users', 'users.id', 'spaceMembers.userId')
.leftJoin('groups', 'groups.id', 'spaceMembers.groupId')
.select([
'users.id as userId',
'users.name as userName',
'users.avatarUrl as userAvatarUrl',
'users.email as userEmail',
'groups.id as groupId',
'groups.name as groupName',
'groups.isDefault as groupIsDefault',
'spaceMembers.role',
'spaceMembers.createdAt',
])
.select((eb) => this.groupRepo.withMemberCount(eb))
.where('spaceId', '=', spaceId)
.orderBy('spaceMembers.createdAt', 'asc');
const result = await executeWithPagination(query, {
page: pagination.page,
perPage: pagination.limit,
});
let memberInfo: MemberInfo;
const members = result.items.map((member) => {
if (member.userId) {
memberInfo = {
id: member.userId,
name: member.userName,
email: member.userEmail,
avatarUrl: member.userAvatarUrl,
type: 'user',
};
} else if (member.groupId) {
memberInfo = {
id: member.groupId,
name: member.groupName,
memberCount: member.memberCount as number,
isDefault: member.groupIsDefault,
type: 'group',
};
}
return {
...memberInfo,
role: member.role,
createdAt: member.createdAt,
};
});
result.items = members as any;
return result;
}
/*
* we want to get a user's role in a space.
* they user can be a member either directly or via a group
* we will pass the user id and space id to return the user's roles
* if the user is a member of the space via multiple groups
* if the user has no space permission it should return an empty array,
* maybe we should throw an exception?
*/
async getUserSpaceRoles(
userId: string,
spaceId: string,
): Promise<UserSpaceRole[]> {
const roles = await this.db
.selectFrom('spaceMembers')
.select(['userId', 'role'])
.where('userId', '=', userId)
.where('spaceId', '=', spaceId)
.unionAll(
this.db
.selectFrom('spaceMembers')
.innerJoin('groupUsers', 'groupUsers.groupId', 'spaceMembers.groupId')
.select(['groupUsers.userId', 'spaceMembers.role'])
.where('groupUsers.userId', '=', userId)
.where('spaceMembers.spaceId', '=', spaceId),
)
.execute();
if (!roles || roles.length === 0) {
return undefined;
}
return roles;
}
}

View File

@ -0,0 +1,143 @@
import { Injectable } from '@nestjs/common';
import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
import { dbOrTx } from '@docmost/db/utils';
import {
InsertableSpace,
Space,
UpdatableSpace,
} from '@docmost/db/types/entity.types';
import { ExpressionBuilder, sql } from 'kysely';
import { PaginationOptions } from '../../pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination';
import { DB } from '@docmost/db/types/db';
@Injectable()
export class SpaceRepo {
constructor(@InjectKysely() private readonly db: KyselyDB) {}
async findById(
spaceId: string,
workspaceId: string,
opts?: { includeMemberCount: boolean },
): Promise<Space> {
return await this.db
.selectFrom('spaces')
.selectAll('spaces')
.$if(opts?.includeMemberCount, (qb) => qb.select(this.withMemberCount))
.where('id', '=', spaceId)
.where('workspaceId', '=', workspaceId)
.executeTakeFirst();
}
async findBySlug(
slug: string,
workspaceId: string,
opts?: { includeMemberCount: boolean },
): Promise<Space> {
return await this.db
.selectFrom('spaces')
.selectAll('spaces')
.$if(opts?.includeMemberCount, (qb) => qb.select(this.withMemberCount))
.where(sql`LOWER(slug)`, '=', sql`LOWER(${slug})`)
.where('workspaceId', '=', workspaceId)
.executeTakeFirst();
}
async slugExists(
slug: string,
workspaceId: string,
trx?: KyselyTransaction,
): Promise<boolean> {
const db = dbOrTx(this.db, trx);
let { count } = await db
.selectFrom('spaces')
.select((eb) => eb.fn.count('id').as('count'))
.where(sql`LOWER(slug)`, '=', sql`LOWER(${slug})`)
.where('workspaceId', '=', workspaceId)
.executeTakeFirst();
count = count as number;
return count != 0;
}
async updateSpace(
updatableSpace: UpdatableSpace,
spaceId: string,
workspaceId: string,
trx?: KyselyTransaction,
) {
const db = dbOrTx(this.db, trx);
return db
.updateTable('spaces')
.set(updatableSpace)
.where('id', '=', spaceId)
.where('workspaceId', '=', workspaceId)
.returningAll()
.executeTakeFirst();
}
async insertSpace(
insertableSpace: InsertableSpace,
trx?: KyselyTransaction,
): Promise<Space> {
const db = dbOrTx(this.db, trx);
return db
.insertInto('spaces')
.values(insertableSpace)
.returningAll()
.executeTakeFirst();
}
async getSpacesInWorkspace(
workspaceId: string,
pagination: PaginationOptions,
) {
// todo: show spaces user have access based on visibility and memberships
let query = this.db
.selectFrom('spaces')
.selectAll('spaces')
.select((eb) => [this.withMemberCount(eb)])
.where('workspaceId', '=', workspaceId)
.orderBy('createdAt', 'asc');
if (pagination.query) {
query = query.where((eb) =>
eb('name', 'ilike', `%${pagination.query}%`).or(
'description',
'ilike',
`%${pagination.query}%`,
),
);
}
const result = executeWithPagination(query, {
page: pagination.page,
perPage: pagination.limit,
});
return result;
}
withMemberCount(eb: ExpressionBuilder<DB, 'spaces'>) {
return eb
.selectFrom('spaceMembers')
.innerJoin('groups', 'groups.id', 'spaceMembers.groupId')
.innerJoin('groupUsers', 'groupUsers.groupId', 'groups.id')
.select((eb) =>
eb.fn
.count(sql`concat(space_members.user_id, group_users.user_id)`)
.distinct()
.as('count'),
)
.whereRef('spaceMembers.spaceId', '=', 'spaces.id')
.as('memberCount');
}
async deleteSpace(spaceId: string, workspaceId: string): Promise<void> {
await this.db
.deleteFrom('spaces')
.where('id', '=', spaceId)
.where('workspaceId', '=', workspaceId)
.execute();
}
}

View File

@ -0,0 +1,22 @@
export interface UserSpaceRole {
userId: string;
role: string;
}
interface SpaceUserInfo {
id: string;
name: string;
email: string;
avatarUrl: string;
type: 'user';
}
interface SpaceGroupInfo {
id: string;
name: string;
isDefault: boolean;
memberCount: number;
type: 'group';
}
export type MemberInfo = SpaceUserInfo | SpaceGroupInfo;

View File

@ -0,0 +1,23 @@
import { UserSpaceRole } from '@docmost/db/repos/space/types';
import { SpaceRole } from '../../../helpers/types/permission';
export function findHighestUserSpaceRole(userSpaceRoles: UserSpaceRole[]) {
if (!userSpaceRoles) {
return undefined;
}
const roleOrder: { [key in SpaceRole]: number } = {
[SpaceRole.ADMIN]: 3,
[SpaceRole.WRITER]: 2,
[SpaceRole.READER]: 1,
};
let highestRole: string;
for (const userSpaceRole of userSpaceRoles) {
const currentRole = userSpaceRole.role;
if (!highestRole || roleOrder[currentRole] > roleOrder[highestRole]) {
highestRole = currentRole;
}
}
return highestRole;
}