mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-22 04:31:06 +10:00
updates and fixes
* seo friendly urls * custom client serve-static module * database fixes * fix recent pages * other fixes
This commit is contained in:
51
apps/server/src/database/repos/attachment/attachment.repo.ts
Normal file
51
apps/server/src/database/repos/attachment/attachment.repo.ts
Normal file
@ -0,0 +1,51 @@
|
||||
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 {
|
||||
Attachment,
|
||||
InsertableAttachment,
|
||||
UpdatableAttachment,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
async findById(
|
||||
attachmentId: string,
|
||||
workspaceId: string,
|
||||
): Promise<Attachment> {
|
||||
return this.db
|
||||
.selectFrom('attachments')
|
||||
.selectAll()
|
||||
.where('id', '=', attachmentId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async insertAttachment(
|
||||
insertableAttachment: InsertableAttachment,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<Attachment> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
|
||||
return db
|
||||
.insertInto('attachments')
|
||||
.values(insertableAttachment)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updateAttachment(
|
||||
updatableAttachment: UpdatableAttachment,
|
||||
attachmentId: string,
|
||||
): Promise<void> {
|
||||
await this.db
|
||||
.updateTable('attachments')
|
||||
.set(updatableAttachment)
|
||||
.where('id', '=', attachmentId)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
}
|
||||
86
apps/server/src/database/repos/comment/comment.repo.ts
Normal file
86
apps/server/src/database/repos/comment/comment.repo.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '../../types/kysely.types';
|
||||
import { dbOrTx } from '../../utils';
|
||||
import {
|
||||
Comment,
|
||||
InsertableComment,
|
||||
UpdatableComment,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||
import { ExpressionBuilder } from 'kysely';
|
||||
import { DB } from '@docmost/db/types/db';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
|
||||
@Injectable()
|
||||
export class CommentRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
// todo, add workspaceId
|
||||
async findById(
|
||||
commentId: string,
|
||||
opts?: { includeCreator: boolean },
|
||||
): Promise<Comment> {
|
||||
return await this.db
|
||||
.selectFrom('comments')
|
||||
.selectAll('comments')
|
||||
.$if(opts?.includeCreator, (qb) => qb.select(this.withCreator))
|
||||
.where('id', '=', commentId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findPageComments(pageId: string, pagination: PaginationOptions) {
|
||||
const query = this.db
|
||||
.selectFrom('comments')
|
||||
.selectAll('comments')
|
||||
.select((eb) => this.withCreator(eb))
|
||||
.where('pageId', '=', pageId)
|
||||
.orderBy('createdAt', 'asc');
|
||||
|
||||
const result = executeWithPagination(query, {
|
||||
page: pagination.page,
|
||||
perPage: pagination.limit,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateComment(
|
||||
updatableComment: UpdatableComment,
|
||||
commentId: string,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
await db
|
||||
.updateTable('comments')
|
||||
.set(updatableComment)
|
||||
.where('id', '=', commentId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async insertComment(
|
||||
insertableComment: InsertableComment,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<Comment> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.insertInto('comments')
|
||||
.values(insertableComment)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
withCreator(eb: ExpressionBuilder<DB, 'comments'>) {
|
||||
return jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('users')
|
||||
.select(['users.id', 'users.name', 'users.avatarUrl'])
|
||||
.whereRef('users.id', '=', 'comments.creatorId'),
|
||||
).as('creator');
|
||||
}
|
||||
|
||||
async deleteComment(commentId: string): Promise<void> {
|
||||
await this.db.deleteFrom('comments').where('id', '=', commentId).execute();
|
||||
}
|
||||
}
|
||||
154
apps/server/src/database/repos/group/group-user.repo.ts
Normal file
154
apps/server/src/database/repos/group/group-user.repo.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { dbOrTx, executeTx } from '@docmost/db/utils';
|
||||
import { GroupUser, InsertableGroupUser } from '@docmost/db/types/entity.types';
|
||||
import { PaginationOptions } from '../../pagination/pagination-options';
|
||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
|
||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
|
||||
@Injectable()
|
||||
export class GroupUserRepo {
|
||||
constructor(
|
||||
@InjectKysely() private readonly db: KyselyDB,
|
||||
private readonly groupRepo: GroupRepo,
|
||||
private readonly userRepo: UserRepo,
|
||||
) {}
|
||||
|
||||
async getGroupUserById(
|
||||
userId: string,
|
||||
groupId: string,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.selectFrom('groupUsers')
|
||||
.selectAll()
|
||||
.where('userId', '=', userId)
|
||||
.where('groupId', '=', groupId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async insertGroupUser(
|
||||
insertableGroupUser: InsertableGroupUser,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<GroupUser> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.insertInto('groupUsers')
|
||||
.values(insertableGroupUser)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async getGroupUsersPaginated(groupId: string, pagination: PaginationOptions) {
|
||||
let query = this.db
|
||||
.selectFrom('groupUsers')
|
||||
.innerJoin('users', 'users.id', 'groupUsers.userId')
|
||||
.selectAll('users')
|
||||
.where('groupId', '=', groupId)
|
||||
.orderBy('createdAt', 'asc');
|
||||
|
||||
if (pagination.query) {
|
||||
query = query.where((eb) =>
|
||||
eb('users.name', 'ilike', `%${pagination.query}%`),
|
||||
);
|
||||
}
|
||||
|
||||
const result = await executeWithPagination(query, {
|
||||
page: pagination.page,
|
||||
perPage: pagination.limit,
|
||||
});
|
||||
|
||||
result.items.map((user) => {
|
||||
delete user.password;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async addUserToGroup(
|
||||
userId: string,
|
||||
groupId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
await executeTx(
|
||||
this.db,
|
||||
async (trx) => {
|
||||
const group = await this.groupRepo.findById(groupId, workspaceId, {
|
||||
trx,
|
||||
});
|
||||
if (!group) {
|
||||
throw new NotFoundException('Group not found');
|
||||
}
|
||||
|
||||
const user = await this.userRepo.findById(userId, workspaceId, {
|
||||
trx: trx,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
const groupUserExists = await this.getGroupUserById(
|
||||
userId,
|
||||
groupId,
|
||||
trx,
|
||||
);
|
||||
|
||||
if (groupUserExists) {
|
||||
throw new BadRequestException(
|
||||
'User is already a member of this group',
|
||||
);
|
||||
}
|
||||
|
||||
await this.insertGroupUser(
|
||||
{
|
||||
userId,
|
||||
groupId,
|
||||
},
|
||||
trx,
|
||||
);
|
||||
},
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
async addUserToDefaultGroup(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
await executeTx(
|
||||
this.db,
|
||||
async (trx) => {
|
||||
const defaultGroup = await this.groupRepo.getDefaultGroup(
|
||||
workspaceId,
|
||||
trx,
|
||||
);
|
||||
await this.insertGroupUser(
|
||||
{
|
||||
userId,
|
||||
groupId: defaultGroup.id,
|
||||
},
|
||||
trx,
|
||||
);
|
||||
},
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
async delete(userId: string, groupId: string): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('groupUsers')
|
||||
.where('userId', '=', userId)
|
||||
.where('groupId', '=', groupId)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
148
apps/server/src/database/repos/group/group.repo.ts
Normal file
148
apps/server/src/database/repos/group/group.repo.ts
Normal file
@ -0,0 +1,148 @@
|
||||
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 {
|
||||
Group,
|
||||
InsertableGroup,
|
||||
UpdatableGroup,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { ExpressionBuilder, sql } from 'kysely';
|
||||
import { PaginationOptions } from '../../pagination/pagination-options';
|
||||
import { DB } from '@docmost/db/types/db';
|
||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||
import { DefaultGroup } from '../../../core/group/dto/create-group.dto';
|
||||
|
||||
@Injectable()
|
||||
export class GroupRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
async findById(
|
||||
groupId: string,
|
||||
workspaceId: string,
|
||||
opts?: { includeMemberCount?: boolean; trx?: KyselyTransaction },
|
||||
): Promise<Group> {
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
return db
|
||||
.selectFrom('groups')
|
||||
.selectAll('groups')
|
||||
.$if(opts?.includeMemberCount, (qb) => qb.select(this.withMemberCount))
|
||||
.where('id', '=', groupId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findByName(
|
||||
groupName: string,
|
||||
workspaceId: string,
|
||||
opts?: { includeMemberCount?: boolean; trx?: KyselyTransaction },
|
||||
): Promise<Group> {
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
return db
|
||||
.selectFrom('groups')
|
||||
.selectAll('groups')
|
||||
.$if(opts?.includeMemberCount, (qb) => qb.select(this.withMemberCount))
|
||||
.where(sql`LOWER(name)`, '=', sql`LOWER(${groupName})`)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async update(
|
||||
updatableGroup: UpdatableGroup,
|
||||
groupId: string,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
await this.db
|
||||
.updateTable('groups')
|
||||
.set(updatableGroup)
|
||||
.where('id', '=', groupId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async insertGroup(
|
||||
insertableGroup: InsertableGroup,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<Group> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.insertInto('groups')
|
||||
.values(insertableGroup)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async getDefaultGroup(
|
||||
workspaceId: string,
|
||||
trx: KyselyTransaction,
|
||||
): Promise<Group> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return (
|
||||
db
|
||||
.selectFrom('groups')
|
||||
.selectAll()
|
||||
// .select((eb) => this.withMemberCount(eb))
|
||||
.where('isDefault', '=', true)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst()
|
||||
);
|
||||
}
|
||||
|
||||
async createDefaultGroup(
|
||||
workspaceId: string,
|
||||
opts?: { userId?: string; trx?: KyselyTransaction },
|
||||
): Promise<Group> {
|
||||
const { userId, trx } = opts;
|
||||
const insertableGroup: InsertableGroup = {
|
||||
name: DefaultGroup.EVERYONE,
|
||||
isDefault: true,
|
||||
creatorId: userId,
|
||||
workspaceId: workspaceId,
|
||||
};
|
||||
|
||||
return this.insertGroup(insertableGroup, trx);
|
||||
}
|
||||
|
||||
async getGroupsPaginated(workspaceId: string, pagination: PaginationOptions) {
|
||||
let query = this.db
|
||||
.selectFrom('groups')
|
||||
.selectAll('groups')
|
||||
.select((eb) => this.withMemberCount(eb))
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.orderBy('memberCount', 'desc')
|
||||
.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, 'groups'>) {
|
||||
return eb
|
||||
.selectFrom('groupUsers')
|
||||
.select((eb) => eb.fn.countAll().as('count'))
|
||||
.whereRef('groupUsers.groupId', '=', 'groups.id')
|
||||
.as('memberCount');
|
||||
}
|
||||
|
||||
async delete(groupId: string, workspaceId: string): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('groups')
|
||||
.where('id', '=', groupId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
79
apps/server/src/database/repos/page/page-history.repo.ts
Normal file
79
apps/server/src/database/repos/page/page-history.repo.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '../../types/kysely.types';
|
||||
import { dbOrTx } from '../../utils';
|
||||
import {
|
||||
InsertablePageHistory,
|
||||
Page,
|
||||
PageHistory,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { ExpressionBuilder } from 'kysely';
|
||||
import { DB } from '@docmost/db/types/db';
|
||||
|
||||
@Injectable()
|
||||
export class PageHistoryRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
async findById(pageHistoryId: string): Promise<PageHistory> {
|
||||
return await this.db
|
||||
.selectFrom('pageHistory')
|
||||
.selectAll()
|
||||
.select((eb) => this.withLastUpdatedBy(eb))
|
||||
.where('id', '=', pageHistoryId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async insertPageHistory(
|
||||
insertablePageHistory: InsertablePageHistory,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<PageHistory> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.insertInto('pageHistory')
|
||||
.values(insertablePageHistory)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async saveHistory(page: Page): Promise<void> {
|
||||
await this.insertPageHistory({
|
||||
pageId: page.id,
|
||||
slugId: page.slugId,
|
||||
title: page.title,
|
||||
content: page.content,
|
||||
icon: page.icon,
|
||||
coverPhoto: page.coverPhoto,
|
||||
lastUpdatedById: page.lastUpdatedById ?? page.creatorId,
|
||||
spaceId: page.spaceId,
|
||||
workspaceId: page.workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async findPageHistoryByPageId(pageId: string, pagination: PaginationOptions) {
|
||||
const query = this.db
|
||||
.selectFrom('pageHistory')
|
||||
.selectAll()
|
||||
.select((eb) => this.withLastUpdatedBy(eb))
|
||||
.where('pageId', '=', pageId)
|
||||
.orderBy('createdAt', 'desc');
|
||||
|
||||
const result = executeWithPagination(query, {
|
||||
page: pagination.page,
|
||||
perPage: pagination.limit,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
withLastUpdatedBy(eb: ExpressionBuilder<DB, 'pageHistory'>) {
|
||||
return jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('users')
|
||||
.select(['users.id', 'users.name', 'users.avatarUrl'])
|
||||
.whereRef('users.id', '=', 'pageHistory.lastUpdatedById'),
|
||||
).as('lastUpdatedBy');
|
||||
}
|
||||
}
|
||||
115
apps/server/src/database/repos/page/page.repo.ts
Normal file
115
apps/server/src/database/repos/page/page.repo.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '../../types/kysely.types';
|
||||
import { dbOrTx } from '../../utils';
|
||||
import {
|
||||
InsertablePage,
|
||||
Page,
|
||||
UpdatablePage,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||
import { validate as isValidUUID } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class PageRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
private baseFields: Array<keyof Page> = [
|
||||
'id',
|
||||
'slugId',
|
||||
'title',
|
||||
'icon',
|
||||
'coverPhoto',
|
||||
'position',
|
||||
'parentPageId',
|
||||
'creatorId',
|
||||
'lastUpdatedById',
|
||||
'spaceId',
|
||||
'workspaceId',
|
||||
'isLocked',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
];
|
||||
|
||||
async findById(
|
||||
pageId: string,
|
||||
opts?: {
|
||||
includeContent?: boolean;
|
||||
includeYdoc?: boolean;
|
||||
},
|
||||
): Promise<Page> {
|
||||
let query = this.db
|
||||
.selectFrom('pages')
|
||||
.select(this.baseFields)
|
||||
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
||||
.$if(opts?.includeYdoc, (qb) => qb.select('ydoc'));
|
||||
|
||||
if (isValidUUID(pageId)) {
|
||||
query = query.where('id', '=', pageId);
|
||||
} else {
|
||||
query = query.where('slugId', '=', pageId);
|
||||
}
|
||||
|
||||
return query.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updatePage(
|
||||
updatablePage: UpdatablePage,
|
||||
pageId: string,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
let query = db.updateTable('pages').set(updatablePage);
|
||||
|
||||
if (isValidUUID(pageId)) {
|
||||
query = query.where('id', '=', pageId);
|
||||
} else {
|
||||
query = query.where('slugId', '=', pageId);
|
||||
}
|
||||
|
||||
return query.executeTakeFirst();
|
||||
}
|
||||
|
||||
async insertPage(
|
||||
insertablePage: InsertablePage,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<Page> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.insertInto('pages')
|
||||
.values(insertablePage)
|
||||
.returning(this.baseFields)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async deletePage(pageId: string): Promise<void> {
|
||||
let query = this.db.deleteFrom('pages');
|
||||
|
||||
if (isValidUUID(pageId)) {
|
||||
query = query.where('id', '=', pageId);
|
||||
} else {
|
||||
query = query.where('slugId', '=', pageId);
|
||||
}
|
||||
|
||||
await query.execute();
|
||||
}
|
||||
|
||||
async getRecentPageUpdates(spaceId: string, pagination: PaginationOptions) {
|
||||
//TODO: should fetch pages from all spaces the user is member of
|
||||
// for now, fetch from default space
|
||||
const query = this.db
|
||||
.selectFrom('pages')
|
||||
.select(this.baseFields)
|
||||
.where('spaceId', '=', spaceId)
|
||||
.orderBy('updatedAt', 'desc');
|
||||
|
||||
const result = executeWithPagination(query, {
|
||||
page: pagination.page,
|
||||
perPage: pagination.limit,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
187
apps/server/src/database/repos/space/space-member.repo.ts
Normal file
187
apps/server/src/database/repos/space/space-member.repo.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
143
apps/server/src/database/repos/space/space.repo.ts
Normal file
143
apps/server/src/database/repos/space/space.repo.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
22
apps/server/src/database/repos/space/types.ts
Normal file
22
apps/server/src/database/repos/space/types.ts
Normal 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;
|
||||
23
apps/server/src/database/repos/space/utils.ts
Normal file
23
apps/server/src/database/repos/space/utils.ts
Normal 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;
|
||||
}
|
||||
152
apps/server/src/database/repos/user/user.repo.ts
Normal file
152
apps/server/src/database/repos/user/user.repo.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { Users } from '@docmost/db/types/db';
|
||||
import { hashPassword } from '../../../helpers';
|
||||
import { dbOrTx } from '@docmost/db/utils';
|
||||
import {
|
||||
InsertableUser,
|
||||
UpdatableUser,
|
||||
User,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { PaginationOptions } from '../../pagination/pagination-options';
|
||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||
|
||||
@Injectable()
|
||||
export class UserRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
public baseFields: Array<keyof Users> = [
|
||||
'id',
|
||||
'email',
|
||||
'name',
|
||||
'emailVerifiedAt',
|
||||
'avatarUrl',
|
||||
'role',
|
||||
'workspaceId',
|
||||
'locale',
|
||||
'timezone',
|
||||
'settings',
|
||||
'lastLoginAt',
|
||||
'deactivatedAt',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
];
|
||||
|
||||
async findById(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
opts?: {
|
||||
includePassword?: boolean;
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
): Promise<User> {
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
return db
|
||||
.selectFrom('users')
|
||||
.select(this.baseFields)
|
||||
.$if(opts?.includePassword, (qb) => qb.select('password'))
|
||||
.where('id', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findByEmail(
|
||||
email: string,
|
||||
workspaceId: string,
|
||||
includePassword?: boolean,
|
||||
): Promise<User> {
|
||||
return this.db
|
||||
.selectFrom('users')
|
||||
.select(this.baseFields)
|
||||
.$if(includePassword, (qb) => qb.select('password'))
|
||||
.where('email', '=', email)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updateUser(
|
||||
updatableUser: UpdatableUser,
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
return await this.db
|
||||
.updateTable('users')
|
||||
.set(updatableUser)
|
||||
.where('id', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async updateLastLogin(userId: string, workspaceId: string) {
|
||||
return await this.db
|
||||
.updateTable('users')
|
||||
.set({
|
||||
lastLoginAt: new Date(),
|
||||
})
|
||||
.where('id', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async insertUser(
|
||||
insertableUser: InsertableUser,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<User> {
|
||||
const user: InsertableUser = {
|
||||
name: insertableUser.name || insertableUser.email.toLowerCase(),
|
||||
email: insertableUser.email.toLowerCase(),
|
||||
password: await hashPassword(insertableUser.password),
|
||||
locale: 'en',
|
||||
role: insertableUser?.role,
|
||||
lastLoginAt: new Date(),
|
||||
};
|
||||
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.insertInto('users')
|
||||
.values(user)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async roleCountByWorkspaceId(
|
||||
role: string,
|
||||
workspaceId: string,
|
||||
): Promise<number> {
|
||||
const { count } = await this.db
|
||||
.selectFrom('users')
|
||||
.select((eb) => eb.fn.count('role').as('count'))
|
||||
.where('role', '=', role)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
|
||||
return count as number;
|
||||
}
|
||||
|
||||
async getUsersPaginated(workspaceId: string, pagination: PaginationOptions) {
|
||||
let query = this.db
|
||||
.selectFrom('users')
|
||||
.select(this.baseFields)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.orderBy('createdAt', 'asc');
|
||||
|
||||
if (pagination.query) {
|
||||
query = query.where((eb) =>
|
||||
eb('users.name', 'ilike', `%${pagination.query}%`).or(
|
||||
'users.email',
|
||||
'ilike',
|
||||
`%${pagination.query}%`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const result = executeWithPagination(query, {
|
||||
page: pagination.page,
|
||||
perPage: pagination.limit,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
73
apps/server/src/database/repos/workspace/workspace.repo.ts
Normal file
73
apps/server/src/database/repos/workspace/workspace.repo.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '../../types/kysely.types';
|
||||
import { dbOrTx } from '../../utils';
|
||||
import {
|
||||
InsertableWorkspace,
|
||||
UpdatableWorkspace,
|
||||
Workspace,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { sql } from 'kysely';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
async findById(workspaceId: string): Promise<Workspace> {
|
||||
return await this.db
|
||||
.selectFrom('workspaces')
|
||||
.selectAll()
|
||||
.where('id', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findFirst(): Promise<Workspace> {
|
||||
return await this.db
|
||||
.selectFrom('workspaces')
|
||||
.selectAll()
|
||||
.orderBy('createdAt asc')
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findByHostname(hostname: string): Promise<Workspace> {
|
||||
return await this.db
|
||||
.selectFrom('workspaces')
|
||||
.selectAll()
|
||||
.where(sql`LOWER(hostname)`, '=', sql`LOWER(${hostname})`)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updateWorkspace(
|
||||
updatableWorkspace: UpdatableWorkspace,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.updateTable('workspaces')
|
||||
.set(updatableWorkspace)
|
||||
.where('id', '=', workspaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async insertWorkspace(
|
||||
insertableWorkspace: InsertableWorkspace,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<Workspace> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.insertInto('workspaces')
|
||||
.values(insertableWorkspace)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async count(): Promise<number> {
|
||||
const { count } = await this.db
|
||||
.selectFrom('workspaces')
|
||||
.select((eb) => eb.fn.count('id').as('count'))
|
||||
.executeTakeFirst();
|
||||
return count as number;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user