mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-12 07:42:34 +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:
1
apps/server/.gitignore
vendored
1
apps/server/.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/storage
|
||||
.env
|
||||
package-lock.json
|
||||
# compiled output
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@ -13,13 +13,13 @@
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"email:dev": "email dev -p 5019 -d ./src/integrations/transactional/emails",
|
||||
"migration:create": "tsx ./src/kysely/migrate.ts create",
|
||||
"migration:up": "tsx ./src/kysely/migrate.ts up",
|
||||
"migration:down": "tsx ./src/kysely/migrate.ts down",
|
||||
"migration:latest": "tsx ./src/kysely/migrate.ts latest",
|
||||
"migration:redo": "tsx ./src/kysely/migrate.ts redo",
|
||||
"migration:reset": "tsx ./src/kysely/migrate.ts down-to NO_MIGRATIONS",
|
||||
"migration:codegen": "kysely-codegen --dialect=postgres --camel-case --env-file=../../.env --out-file=./src/kysely/types/db.d.ts",
|
||||
"migration:create": "tsx src/database/migrate.ts create",
|
||||
"migration:up": "tsx src/database/migrate.ts up",
|
||||
"migration:down": "tsx src/database/migrate.ts down",
|
||||
"migration:latest": "tsx src/database/migrate.ts latest",
|
||||
"migration:redo": "tsx src/database/migrate.ts redo",
|
||||
"migration:reset": "tsx src/database/migrate.ts down-to NO_MIGRATIONS",
|
||||
"migration:codegen": "kysely-codegen --dialect=postgres --camel-case --env-file=../../.env --out-file=./src/database/types/db.d.ts",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
@ -42,7 +42,6 @@
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-fastify": "^10.3.8",
|
||||
"@nestjs/platform-socket.io": "^10.3.8",
|
||||
"@nestjs/serve-static": "^4.0.2",
|
||||
"@nestjs/websockets": "^10.3.8",
|
||||
"@react-email/components": "0.0.17",
|
||||
"@react-email/render": "^0.0.13",
|
||||
@ -68,7 +67,6 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-filename-ts": "^1.0.2",
|
||||
"slugify": "^1.6.6",
|
||||
"socket.io": "^4.7.5",
|
||||
"tsx": "^4.8.2",
|
||||
"uuid": "^9.0.1",
|
||||
|
||||
@ -5,26 +5,11 @@ import { CoreModule } from './core/core.module';
|
||||
import { EnvironmentModule } from './integrations/environment/environment.module';
|
||||
import { CollaborationModule } from './collaboration/collaboration.module';
|
||||
import { WsModule } from './ws/ws.module';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { join } from 'path';
|
||||
import { DatabaseModule } from '@docmost/db/database.module';
|
||||
import * as fs from 'fs';
|
||||
import { StorageModule } from './integrations/storage/storage.module';
|
||||
import { MailModule } from './integrations/mail/mail.module';
|
||||
import { QueueModule } from './integrations/queue/queue.module';
|
||||
|
||||
const clientDistPath = join(__dirname, '..', '..', 'client/dist');
|
||||
|
||||
function getServeStaticModule() {
|
||||
if (fs.existsSync(clientDistPath)) {
|
||||
return [
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: clientDistPath,
|
||||
}),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
import { StaticModule } from './integrations/static/static.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -34,7 +19,7 @@ function getServeStaticModule() {
|
||||
CollaborationModule,
|
||||
WsModule,
|
||||
QueueModule,
|
||||
...getServeStaticModule(),
|
||||
StaticModule,
|
||||
StorageModule.forRootAsync({
|
||||
imports: [EnvironmentModule],
|
||||
}),
|
||||
|
||||
@ -78,6 +78,7 @@ export class PersistenceExtension implements Extension {
|
||||
textContent: textContent,
|
||||
ydoc: ydocState,
|
||||
lastUpdatedById: context.user.id,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
pageId,
|
||||
);
|
||||
|
||||
@ -9,6 +9,7 @@ import { executeTx } from '@docmost/db/utils';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { User } from '@docmost/db/types/entity.types';
|
||||
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
|
||||
import { UserRole } from '../../../helpers/types/permission';
|
||||
|
||||
@Injectable()
|
||||
export class SignupService {
|
||||
@ -75,7 +76,11 @@ export class SignupService {
|
||||
this.db,
|
||||
async (trx) => {
|
||||
// create user
|
||||
const user = await this.userRepo.insertUser(createAdminUserDto, trx);
|
||||
|
||||
const user = await this.userRepo.insertUser(
|
||||
{ ...createAdminUserDto, role: UserRole.OWNER },
|
||||
trx,
|
||||
);
|
||||
|
||||
// create workspace with full setup
|
||||
const workspaceData: CreateWorkspaceDto = {
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
AbilityBuilder,
|
||||
createMongoAbility,
|
||||
@ -33,9 +37,7 @@ export default class SpaceAbilityFactory {
|
||||
case SpaceRole.READER:
|
||||
return buildSpaceReaderAbility();
|
||||
default:
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to access this space',
|
||||
);
|
||||
throw new NotFoundException('Space permissions not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +52,12 @@ export class CommentController {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.commentService.create(user.id, workspace.id, createCommentDto);
|
||||
return this.commentService.create(
|
||||
user.id,
|
||||
page.id,
|
||||
workspace.id,
|
||||
createCommentDto,
|
||||
);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ -73,7 +78,7 @@ export class CommentController {
|
||||
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return this.commentService.findByPageId(input.pageId, pagination);
|
||||
return this.commentService.findByPageId(page.id, pagination);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ -84,7 +89,6 @@ export class CommentController {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
|
||||
// TODO: add spaceId to comment entity.
|
||||
const page = await this.pageRepo.findById(comment.pageId);
|
||||
if (!page) {
|
||||
throw new NotFoundException('Page not found');
|
||||
@ -104,6 +108,7 @@ export class CommentController {
|
||||
return this.commentService.update(
|
||||
updateCommentDto.commentId,
|
||||
updateCommentDto,
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@ -111,6 +116,6 @@ export class CommentController {
|
||||
@Post('delete')
|
||||
remove(@Body() input: CommentIdDto, @AuthUser() user: User) {
|
||||
// TODO: only comment creators and admins can delete their comments
|
||||
return this.commentService.remove(input.commentId);
|
||||
return this.commentService.remove(input.commentId, user);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { CreateCommentDto } from './dto/create-comment.dto';
|
||||
import { UpdateCommentDto } from './dto/update-comment.dto';
|
||||
import { CommentRepo } from '@docmost/db/repos/comment/comment.repo';
|
||||
import { Comment } from '@docmost/db/types/entity.types';
|
||||
import { Comment, User } from '@docmost/db/types/entity.types';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { PaginationResult } from '@docmost/db/pagination/pagination';
|
||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||
@ -30,24 +31,18 @@ export class CommentService {
|
||||
|
||||
async create(
|
||||
userId: string,
|
||||
pageId: string,
|
||||
workspaceId: string,
|
||||
createCommentDto: CreateCommentDto,
|
||||
) {
|
||||
const commentContent = JSON.parse(createCommentDto.content);
|
||||
|
||||
const page = await this.pageRepo.findById(createCommentDto.pageId);
|
||||
// const spaceId = null; // todo, get from page
|
||||
|
||||
if (!page) {
|
||||
throw new BadRequestException('Page not found');
|
||||
}
|
||||
|
||||
if (createCommentDto.parentCommentId) {
|
||||
const parentComment = await this.commentRepo.findById(
|
||||
createCommentDto.parentCommentId,
|
||||
);
|
||||
|
||||
if (!parentComment) {
|
||||
if (!parentComment || parentComment.pageId !== pageId) {
|
||||
throw new BadRequestException('Parent comment not found');
|
||||
}
|
||||
|
||||
@ -57,10 +52,10 @@ export class CommentService {
|
||||
}
|
||||
|
||||
const createdComment = await this.commentRepo.insertComment({
|
||||
pageId: createCommentDto.pageId,
|
||||
pageId: pageId,
|
||||
content: commentContent,
|
||||
selection: createCommentDto?.selection?.substring(0, 250),
|
||||
type: 'inline', // for now
|
||||
type: 'inline',
|
||||
parentCommentId: createCommentDto?.parentCommentId,
|
||||
creatorId: userId,
|
||||
workspaceId: workspaceId,
|
||||
@ -90,6 +85,7 @@ export class CommentService {
|
||||
async update(
|
||||
commentId: string,
|
||||
updateCommentDto: UpdateCommentDto,
|
||||
authUser: User,
|
||||
): Promise<Comment> {
|
||||
const commentContent = JSON.parse(updateCommentDto.content);
|
||||
|
||||
@ -98,6 +94,10 @@ export class CommentService {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
|
||||
if (comment.creatorId !== authUser.id) {
|
||||
throw new ForbiddenException('You can only edit your own comments');
|
||||
}
|
||||
|
||||
const editedAt = new Date();
|
||||
|
||||
await this.commentRepo.updateComment(
|
||||
@ -113,12 +113,17 @@ export class CommentService {
|
||||
return comment;
|
||||
}
|
||||
|
||||
async remove(commentId: string): Promise<void> {
|
||||
async remove(commentId: string, authUser: User): Promise<void> {
|
||||
const comment = await this.commentRepo.findById(commentId);
|
||||
|
||||
if (!comment) {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
|
||||
if (comment.creatorId !== authUser.id) {
|
||||
throw new ForbiddenException('You can only delete your own comments');
|
||||
}
|
||||
|
||||
await this.commentRepo.deleteComment(commentId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { IsUUID } from 'class-validator';
|
||||
import { IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export class PageIdDto {
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { IsJSON, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export class CreateCommentDto {
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
pageId: string;
|
||||
|
||||
@IsJSON()
|
||||
|
||||
@ -10,7 +10,7 @@ export class CreatePageDto {
|
||||
icon?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
parentPageId?: string;
|
||||
|
||||
@IsUUID()
|
||||
|
||||
@ -1,13 +1,7 @@
|
||||
import {
|
||||
IsString,
|
||||
IsUUID,
|
||||
IsOptional,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
import { IsString, IsOptional, MinLength, MaxLength } from 'class-validator';
|
||||
|
||||
export class MovePageDto {
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
pageId: string;
|
||||
|
||||
@IsString()
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
import { Page } from '@docmost/db/types/entity.types';
|
||||
|
||||
export type PageWithOrderingDto = Page & { childrenIds?: string[] };
|
||||
@ -1,7 +1,7 @@
|
||||
import { IsUUID } from 'class-validator';
|
||||
import { IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export class PageIdDto {
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { IsOptional, IsUUID } from 'class-validator';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { SpaceIdDto } from './page.dto';
|
||||
|
||||
export class SidebarPageDto extends SpaceIdDto {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreatePageDto } from './create-page.dto';
|
||||
import { IsUUID } from 'class-validator';
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class UpdatePageDto extends PartialType(CreatePageDto) {
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import { PageService } from './services/page.service';
|
||||
import { CreatePageDto } from './dto/create-page.dto';
|
||||
import { UpdatePageDto } from './dto/update-page.dto';
|
||||
import { MovePageDto } from './dto/move-page.dto';
|
||||
import { PageHistoryIdDto, PageIdDto, SpaceIdDto } from './dto/page.dto';
|
||||
import { PageHistoryIdDto, PageIdDto } from './dto/page.dto';
|
||||
import { PageHistoryService } from './services/page-history.service';
|
||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||
@ -118,18 +118,23 @@ export class PageController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('recent')
|
||||
async getRecentSpacePages(
|
||||
@Body() spaceIdDto: SpaceIdDto,
|
||||
@Body() pagination: PaginationOptions,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = await this.spaceAbility.createForUser(
|
||||
user,
|
||||
spaceIdDto.spaceId,
|
||||
workspace.defaultSpaceId,
|
||||
);
|
||||
|
||||
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return this.pageService.getRecentSpacePages(spaceIdDto.spaceId, pagination);
|
||||
|
||||
return this.pageService.getRecentSpacePages(
|
||||
workspace.defaultSpaceId,
|
||||
pagination,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: scope to workspaces
|
||||
@ -146,7 +151,7 @@ export class PageController {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.pageHistoryService.findHistoryByPageId(dto.pageId, pagination);
|
||||
return this.pageHistoryService.findHistoryByPageId(page.id, pagination);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ -181,7 +186,17 @@ export class PageController {
|
||||
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return this.pageService.getSidebarPages(dto, pagination);
|
||||
|
||||
let pageId = null;
|
||||
if (dto.pageId) {
|
||||
const page = await this.pageRepo.findById(dto.pageId);
|
||||
if (page.spaceId !== dto.spaceId) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
pageId = page.id;
|
||||
}
|
||||
|
||||
return this.pageService.getSidebarPages(dto.spaceId, pagination, pageId);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ -207,10 +222,14 @@ export class PageController {
|
||||
@Post('/breadcrumbs')
|
||||
async getPageBreadcrumbs(@Body() dto: PageIdDto, @AuthUser() user: User) {
|
||||
const page = await this.pageRepo.findById(dto.pageId);
|
||||
if (!page) {
|
||||
throw new NotFoundException('Page not found');
|
||||
}
|
||||
|
||||
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
|
||||
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return this.pageService.getPageBreadCrumbs(dto.pageId);
|
||||
return this.pageService.getPageBreadCrumbs(page.id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ import { generateJitteredKeyBetween } from 'fractional-indexing-jittered';
|
||||
import { MovePageDto } from '../dto/move-page.dto';
|
||||
import { ExpressionBuilder } from 'kysely';
|
||||
import { DB } from '@docmost/db/types/db';
|
||||
import { SidebarPageDto } from '../dto/sidebar-page.dto';
|
||||
import { genPageShortId } from '../../../helpers/nanoid.utils';
|
||||
|
||||
@Injectable()
|
||||
export class PageService {
|
||||
@ -40,14 +40,19 @@ export class PageService {
|
||||
workspaceId: string,
|
||||
createPageDto: CreatePageDto,
|
||||
): Promise<Page> {
|
||||
let parentPageId = undefined;
|
||||
|
||||
// check if parent page exists
|
||||
if (createPageDto.parentPageId) {
|
||||
const parentPage = await this.pageRepo.findById(
|
||||
createPageDto.parentPageId,
|
||||
);
|
||||
|
||||
if (!parentPage || parentPage.spaceId !== createPageDto.spaceId)
|
||||
if (!parentPage || parentPage.spaceId !== createPageDto.spaceId) {
|
||||
throw new NotFoundException('Parent page not found');
|
||||
}
|
||||
|
||||
parentPageId = parentPage.id;
|
||||
}
|
||||
|
||||
let pagePosition: string;
|
||||
@ -59,10 +64,10 @@ export class PageService {
|
||||
.orderBy('position', 'desc')
|
||||
.limit(1);
|
||||
|
||||
if (createPageDto.parentPageId) {
|
||||
if (parentPageId) {
|
||||
// check for children of this page
|
||||
const lastPage = await lastPageQuery
|
||||
.where('parentPageId', '=', createPageDto.parentPageId)
|
||||
.where('parentPageId', '=', parentPageId)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!lastPage) {
|
||||
@ -87,10 +92,11 @@ export class PageService {
|
||||
}
|
||||
|
||||
const createdPage = await this.pageRepo.insertPage({
|
||||
slugId: genPageShortId(),
|
||||
title: createPageDto.title,
|
||||
position: pagePosition,
|
||||
icon: createPageDto.icon,
|
||||
parentPageId: createPageDto.parentPageId,
|
||||
parentPageId: parentPageId,
|
||||
spaceId: createPageDto.spaceId,
|
||||
creatorId: userId,
|
||||
workspaceId: workspaceId,
|
||||
@ -110,6 +116,7 @@ export class PageService {
|
||||
title: updatePageDto.title,
|
||||
icon: updatePageDto.icon,
|
||||
lastUpdatedById: userId,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
pageId,
|
||||
);
|
||||
@ -135,13 +142,15 @@ export class PageService {
|
||||
}
|
||||
|
||||
async getSidebarPages(
|
||||
dto: SidebarPageDto,
|
||||
spaceId: string,
|
||||
pagination: PaginationOptions,
|
||||
pageId?: string,
|
||||
): Promise<any> {
|
||||
let query = this.db
|
||||
.selectFrom('pages')
|
||||
.select([
|
||||
'id',
|
||||
'slugId',
|
||||
'title',
|
||||
'icon',
|
||||
'position',
|
||||
@ -151,10 +160,10 @@ export class PageService {
|
||||
])
|
||||
.select((eb) => this.withHasChildren(eb))
|
||||
.orderBy('position', 'asc')
|
||||
.where('spaceId', '=', dto.spaceId);
|
||||
.where('spaceId', '=', spaceId);
|
||||
|
||||
if (dto.pageId) {
|
||||
query = query.where('parentPageId', '=', dto.pageId);
|
||||
if (pageId) {
|
||||
query = query.where('parentPageId', '=', pageId);
|
||||
} else {
|
||||
query = query.where('parentPageId', 'is', null);
|
||||
}
|
||||
@ -185,8 +194,8 @@ export class PageService {
|
||||
if (!parentPage || parentPage.spaceId !== movedPage.spaceId) {
|
||||
throw new NotFoundException('Parent page not found');
|
||||
}
|
||||
parentPageId = parentPage.id;
|
||||
}
|
||||
parentPageId = dto.parentPageId;
|
||||
}
|
||||
|
||||
await this.pageRepo.updatePage(
|
||||
@ -205,6 +214,7 @@ export class PageService {
|
||||
.selectFrom('pages')
|
||||
.select([
|
||||
'id',
|
||||
'slugId',
|
||||
'title',
|
||||
'icon',
|
||||
'position',
|
||||
@ -218,6 +228,7 @@ export class PageService {
|
||||
.selectFrom('pages as p')
|
||||
.select([
|
||||
'p.id',
|
||||
'p.slugId',
|
||||
'p.title',
|
||||
'p.icon',
|
||||
'p.position',
|
||||
@ -255,10 +266,7 @@ export class PageService {
|
||||
spaceId: string,
|
||||
pagination: PaginationOptions,
|
||||
): Promise<PaginationResult<Page>> {
|
||||
const pages = await this.pageRepo.getRecentPagesInSpace(
|
||||
spaceId,
|
||||
pagination,
|
||||
);
|
||||
const pages = await this.pageRepo.getRecentPageUpdates(spaceId, pagination);
|
||||
|
||||
return pages;
|
||||
}
|
||||
@ -267,6 +275,7 @@ export class PageService {
|
||||
await this.pageRepo.deletePage(pageId);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO: page deletion and restoration
|
||||
async delete(pageId: string): Promise<void> {
|
||||
|
||||
@ -5,12 +5,13 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { CreateSpaceDto } from '../dto/create-space.dto';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import slugify from 'slugify';
|
||||
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
||||
import { KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { Space } from '@docmost/db/types/entity.types';
|
||||
import { PaginationResult } from '@docmost/db/pagination/pagination';
|
||||
import { UpdateSpaceDto } from '../dto/update-space.dto';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { slugify } = require('fix-esm').require('@sindresorhus/slugify');
|
||||
|
||||
@Injectable()
|
||||
export class SpaceService {
|
||||
|
||||
@ -36,12 +36,16 @@ export class WorkspaceController {
|
||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/public')
|
||||
async getWorkspacePublicInfo(@Req() req) {
|
||||
return this.workspaceService.getWorkspacePublicData(req.raw.workspaceId);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/info')
|
||||
async getWorkspace(
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
async getWorkspace(@AuthWorkspace() workspace: Workspace) {
|
||||
return this.workspaceService.getWorkspaceInfo(workspace.id);
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +46,19 @@ export class WorkspaceService {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
async getWorkspacePublicData(workspaceId: string) {
|
||||
const workspace = await this.db
|
||||
.selectFrom('workspaces')
|
||||
.select(['id'])
|
||||
.where('id', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
if (!workspace) {
|
||||
throw new NotFoundException('Workspace not found');
|
||||
}
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
async create(
|
||||
user: User,
|
||||
createWorkspaceDto: CreateWorkspaceDto,
|
||||
|
||||
@ -8,20 +8,17 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn('name', 'varchar', (col) => col)
|
||||
.addColumn('description', 'text', (col) => col)
|
||||
.addColumn('description', 'varchar', (col) => col)
|
||||
.addColumn('logo', 'varchar', (col) => col)
|
||||
.addColumn('hostname', 'varchar', (col) => col)
|
||||
.addColumn('custom_domain', 'varchar', (col) => col)
|
||||
.addColumn('enable_invite', 'boolean', (col) =>
|
||||
col.defaultTo(true).notNull(),
|
||||
)
|
||||
.addColumn('invite_code', 'varchar', (col) =>
|
||||
col.defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn('settings', 'jsonb', (col) => col)
|
||||
.addColumn('default_role', 'varchar', (col) =>
|
||||
col.defaultTo(UserRole.MEMBER).notNull(),
|
||||
)
|
||||
.addColumn('allowed_email_domains', sql`varchar[]`, (col) =>
|
||||
col.defaultTo('{}'),
|
||||
)
|
||||
.addColumn('default_space_id', 'uuid', (col) => col)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
@ -31,7 +28,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
)
|
||||
.addColumn('deleted_at', 'timestamptz', (col) => col)
|
||||
.addUniqueConstraint('workspaces_hostname_unique', ['hostname'])
|
||||
.addUniqueConstraint('workspaces_invite_code_unique', ['invite_code'])
|
||||
.addUniqueConstraint('workspaces_custom_domain_unique', ['custom_domain'])
|
||||
.execute();
|
||||
}
|
||||
|
||||
@ -9,10 +9,12 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
.addColumn('name', 'varchar', (col) => col)
|
||||
.addColumn('email', 'varchar', (col) => col.notNull())
|
||||
.addColumn('email_verified_at', 'timestamptz', (col) => col)
|
||||
.addColumn('password', 'varchar', (col) => col.notNull())
|
||||
.addColumn('password', 'varchar', (col) => col)
|
||||
.addColumn('avatar_url', 'varchar', (col) => col)
|
||||
.addColumn('role', 'varchar', (col) => col)
|
||||
.addColumn('status', 'varchar', (col) => col)
|
||||
.addColumn('role', 'varchar', (col) => col.notNull())
|
||||
.addColumn('invited_by_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.references('workspaces.id').onDelete('cascade'),
|
||||
)
|
||||
@ -21,12 +23,14 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
.addColumn('settings', 'jsonb', (col) => col)
|
||||
.addColumn('last_active_at', 'timestamptz', (col) => col)
|
||||
.addColumn('last_login_at', 'timestamptz', (col) => col)
|
||||
.addColumn('deactivated_at', 'timestamptz', (col) => col)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('deleted_at', 'timestamptz', (col) => col)
|
||||
.addUniqueConstraint('users_email_workspace_id_unique', [
|
||||
'email',
|
||||
'workspace_id',
|
||||
@ -49,7 +49,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
col.references('spaces.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('role', 'varchar', (col) => col.notNull())
|
||||
.addColumn('addedById', 'uuid', (col) => col.references('users.id'))
|
||||
.addColumn('added_by_id', 'uuid', (col) => col.references('users.id'))
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
@ -6,19 +6,24 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn('email', 'varchar', (col) => col)
|
||||
.addColumn('role', 'varchar', (col) => col.notNull())
|
||||
.addColumn('token', 'varchar', (col) => col.notNull())
|
||||
.addColumn('group_ids', sql`uuid[]`, (col) => col)
|
||||
.addColumn('invited_by_id', 'uuid', (col) => col.references('users.id'))
|
||||
.addColumn('workspace_id', 'uuid', (col) =>
|
||||
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('invited_by_id', 'uuid', (col) => col.references('users.id'))
|
||||
.addColumn('email', 'varchar', (col) => col.notNull())
|
||||
.addColumn('role', 'varchar', (col) => col.notNull())
|
||||
.addColumn('status', 'varchar', (col) => col)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addUniqueConstraint('invitations_email_workspace_id_unique', [
|
||||
'email',
|
||||
'workspace_id',
|
||||
])
|
||||
.execute();
|
||||
}
|
||||
|
||||
@ -6,17 +6,15 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
.addColumn('id', 'uuid', (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn('slug_id', 'varchar', (col) => col.notNull())
|
||||
.addColumn('title', 'varchar', (col) => col)
|
||||
.addColumn('icon', 'varchar', (col) => col)
|
||||
.addColumn('key', 'varchar', (col) => col)
|
||||
.addColumn('cover_photo', 'varchar', (col) => col)
|
||||
.addColumn('position', 'varchar', (col) => col)
|
||||
.addColumn('content', 'jsonb', (col) => col)
|
||||
.addColumn('html', 'text', (col) => col)
|
||||
.addColumn('ydoc', 'bytea', (col) => col)
|
||||
.addColumn('text_content', 'text', (col) => col)
|
||||
.addColumn('tsv', sql`tsvector`, (col) => col)
|
||||
.addColumn('ydoc', 'bytea', (col) => col)
|
||||
.addColumn('slug', 'varchar', (col) => col)
|
||||
.addColumn('cover_photo', 'varchar', (col) => col)
|
||||
.addColumn('editor', 'varchar', (col) => col)
|
||||
.addColumn('parent_page_id', 'uuid', (col) =>
|
||||
col.references('pages.id').onDelete('cascade'),
|
||||
)
|
||||
@ -32,8 +30,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('is_locked', 'boolean', (col) => col.defaultTo(false).notNull())
|
||||
.addColumn('status', 'varchar', (col) => col)
|
||||
.addColumn('published_at', 'date', (col) => col)
|
||||
.addColumn('created_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
@ -41,6 +37,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn('deleted_at', 'timestamptz', (col) => col)
|
||||
.addUniqueConstraint('pages_slug_id_unique', ['slug_id'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
@ -49,38 +46,14 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
.using('GIN')
|
||||
.column('tsv')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('pages_slug_id_idx')
|
||||
.on('pages')
|
||||
.column('slug_id')
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.alterTable('pages')
|
||||
.dropConstraint('pages_creator_id_fkey')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('pages')
|
||||
.dropConstraint('pages_last_updated_by_id_fkey')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('pages')
|
||||
.dropConstraint('pages_deleted_by_id_fkey')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('pages')
|
||||
.dropConstraint('pages_space_id_fkey')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('pages')
|
||||
.dropConstraint('pages_workspace_id_fkey')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('pages')
|
||||
.dropConstraint('pages_parent_page_id_fkey')
|
||||
.execute();
|
||||
|
||||
await db.schema.dropTable('pages').execute();
|
||||
}
|
||||
@ -9,12 +9,13 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
.addColumn('page_id', 'uuid', (col) =>
|
||||
col.references('pages.id').onDelete('cascade').notNull(),
|
||||
)
|
||||
.addColumn('slug_id', 'varchar', (col) => col)
|
||||
.addColumn('title', 'varchar', (col) => col)
|
||||
.addColumn('content', 'jsonb', (col) => col)
|
||||
.addColumn('slug', 'varchar', (col) => col)
|
||||
.addColumn('icon', 'varchar', (col) => col)
|
||||
.addColumn('cover_photo', 'varchar', (col) => col)
|
||||
.addColumn('version', 'int4', (col) => col.notNull())
|
||||
.addColumn('version', 'int4', (col) => col)
|
||||
.addColumn('last_updated_by_id', 'uuid', (col) =>
|
||||
col.references('users.id'),
|
||||
)
|
||||
@ -6,7 +6,6 @@ import {
|
||||
InsertablePageHistory,
|
||||
Page,
|
||||
PageHistory,
|
||||
UpdatablePageHistory,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||
@ -27,19 +26,6 @@ export class PageHistoryRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updatePageHistory(
|
||||
updatablePageHistory: UpdatablePageHistory,
|
||||
pageHistoryId: string,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.updateTable('pageHistory')
|
||||
.set(updatablePageHistory)
|
||||
.where('id', '=', pageHistoryId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async insertPageHistory(
|
||||
insertablePageHistory: InsertablePageHistory,
|
||||
trx?: KyselyTransaction,
|
||||
@ -55,11 +41,10 @@ export class PageHistoryRepo {
|
||||
async saveHistory(page: Page): Promise<void> {
|
||||
await this.insertPageHistory({
|
||||
pageId: page.id,
|
||||
slugId: page.slugId,
|
||||
title: page.title,
|
||||
content: page.content,
|
||||
slug: page.slug,
|
||||
icon: page.icon,
|
||||
version: 1, // TODO: make incremental
|
||||
coverPhoto: page.coverPhoto,
|
||||
lastUpdatedById: page.lastUpdatedById ?? page.creatorId,
|
||||
spaceId: page.spaceId,
|
||||
@ -7,22 +7,20 @@ import {
|
||||
Page,
|
||||
UpdatablePage,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { sql } from 'kysely';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||
import { validate as isValidUUID } from 'uuid';
|
||||
|
||||
// TODO: scope to space/workspace
|
||||
@Injectable()
|
||||
export class PageRepo {
|
||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||
|
||||
private baseFields: Array<keyof Page> = [
|
||||
'id',
|
||||
'slugId',
|
||||
'title',
|
||||
'slug',
|
||||
'icon',
|
||||
'coverPhoto',
|
||||
'key',
|
||||
'position',
|
||||
'parentPageId',
|
||||
'creatorId',
|
||||
@ -30,8 +28,6 @@ export class PageRepo {
|
||||
'spaceId',
|
||||
'workspaceId',
|
||||
'isLocked',
|
||||
'status',
|
||||
'publishedAt',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
@ -44,21 +40,19 @@ export class PageRepo {
|
||||
includeYdoc?: boolean;
|
||||
},
|
||||
): Promise<Page> {
|
||||
return await this.db
|
||||
let query = this.db
|
||||
.selectFrom('pages')
|
||||
.select(this.baseFields)
|
||||
.where('id', '=', pageId)
|
||||
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
||||
.$if(opts?.includeYdoc, (qb) => qb.select('ydoc'))
|
||||
.executeTakeFirst();
|
||||
}
|
||||
.$if(opts?.includeYdoc, (qb) => qb.select('ydoc'));
|
||||
|
||||
async slug(slug: string): Promise<Page> {
|
||||
return await this.db
|
||||
.selectFrom('pages')
|
||||
.selectAll()
|
||||
.where(sql`LOWER(slug)`, '=', sql`LOWER(${slug})`)
|
||||
.executeTakeFirst();
|
||||
if (isValidUUID(pageId)) {
|
||||
query = query.where('id', '=', pageId);
|
||||
} else {
|
||||
query = query.where('slugId', '=', pageId);
|
||||
}
|
||||
|
||||
return query.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updatePage(
|
||||
@ -67,11 +61,15 @@ export class PageRepo {
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.updateTable('pages')
|
||||
.set(updatablePage)
|
||||
.where('id', '=', pageId)
|
||||
.executeTakeFirst();
|
||||
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(
|
||||
@ -87,10 +85,20 @@ export class PageRepo {
|
||||
}
|
||||
|
||||
async deletePage(pageId: string): Promise<void> {
|
||||
await this.db.deleteFrom('pages').where('id', '=', pageId).execute();
|
||||
let query = this.db.deleteFrom('pages');
|
||||
|
||||
if (isValidUUID(pageId)) {
|
||||
query = query.where('id', '=', pageId);
|
||||
} else {
|
||||
query = query.where('slugId', '=', pageId);
|
||||
}
|
||||
|
||||
await query.execute();
|
||||
}
|
||||
|
||||
async getRecentPagesInSpace(spaceId: string, pagination: PaginationOptions) {
|
||||
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)
|
||||
@ -28,8 +28,10 @@ export class UserRepo {
|
||||
'timezone',
|
||||
'settings',
|
||||
'lastLoginAt',
|
||||
'deactivatedAt',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
];
|
||||
|
||||
async findById(
|
||||
@ -97,6 +99,7 @@ export class UserRepo {
|
||||
email: insertableUser.email.toLowerCase(),
|
||||
password: await hashPassword(insertableUser.password),
|
||||
locale: 'en',
|
||||
role: insertableUser?.role,
|
||||
lastLoginAt: new Date(),
|
||||
};
|
||||
|
||||
@ -79,10 +79,11 @@ export interface PageHistory {
|
||||
lastUpdatedById: string | null;
|
||||
pageId: string;
|
||||
slug: string | null;
|
||||
slugId: string | null;
|
||||
spaceId: string;
|
||||
title: string | null;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
version: number;
|
||||
version: number | null;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
@ -93,19 +94,14 @@ export interface Pages {
|
||||
creatorId: string | null;
|
||||
deletedAt: Timestamp | null;
|
||||
deletedById: string | null;
|
||||
editor: string | null;
|
||||
html: string | null;
|
||||
icon: string | null;
|
||||
id: Generated<string>;
|
||||
isLocked: Generated<boolean>;
|
||||
key: string | null;
|
||||
lastUpdatedById: string | null;
|
||||
parentPageId: string | null;
|
||||
position: string | null;
|
||||
publishedAt: Timestamp | null;
|
||||
slug: string | null;
|
||||
slugId: string;
|
||||
spaceId: string;
|
||||
status: string | null;
|
||||
textContent: string | null;
|
||||
title: string | null;
|
||||
tsv: string | null;
|
||||
@ -115,8 +111,8 @@ export interface Pages {
|
||||
}
|
||||
|
||||
export interface SpaceMembers {
|
||||
addedById: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
creatorId: string | null;
|
||||
groupId: string | null;
|
||||
id: Generated<string>;
|
||||
role: string;
|
||||
@ -143,6 +139,8 @@ export interface Spaces {
|
||||
export interface Users {
|
||||
avatarUrl: string | null;
|
||||
createdAt: Generated<Timestamp>;
|
||||
deactivatedAt: Timestamp | null;
|
||||
deletedAt: Timestamp | null;
|
||||
email: string;
|
||||
emailVerifiedAt: Timestamp | null;
|
||||
id: Generated<string>;
|
||||
@ -151,10 +149,9 @@ export interface Users {
|
||||
lastLoginAt: Timestamp | null;
|
||||
locale: string | null;
|
||||
name: string | null;
|
||||
password: string;
|
||||
role: string | null;
|
||||
password: string | null;
|
||||
role: string;
|
||||
settings: Json | null;
|
||||
status: string | null;
|
||||
timezone: string | null;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
workspaceId: string | null;
|
||||
@ -162,27 +159,26 @@ export interface Users {
|
||||
|
||||
export interface WorkspaceInvitations {
|
||||
createdAt: Generated<Timestamp>;
|
||||
email: string;
|
||||
email: string | null;
|
||||
groupIds: string[] | null;
|
||||
id: Generated<string>;
|
||||
invitedById: string | null;
|
||||
role: string;
|
||||
token: string | null;
|
||||
token: string;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export interface Workspaces {
|
||||
allowedEmailDomains: Generated<string[] | null>;
|
||||
createdAt: Generated<Timestamp>;
|
||||
customDomain: string | null;
|
||||
defaultRole: Generated<string>;
|
||||
defaultSpaceId: string | null;
|
||||
deletedAt: Timestamp | null;
|
||||
description: string | null;
|
||||
enableInvite: Generated<boolean>;
|
||||
hostname: string | null;
|
||||
id: Generated<string>;
|
||||
inviteCode: Generated<string | null>;
|
||||
logo: string | null;
|
||||
name: string | null;
|
||||
settings: Json | null;
|
||||
@ -3,3 +3,7 @@ const { customAlphabet } = require('fix-esm').require('nanoid');
|
||||
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
export const nanoIdGen = customAlphabet(alphabet, 10);
|
||||
|
||||
const slugIdAlphabet =
|
||||
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
export const genPageShortId = customAlphabet(slugIdAlphabet, 12);
|
||||
|
||||
57
apps/server/src/integrations/static/static.module.ts
Normal file
57
apps/server/src/integrations/static/static.module.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { Module, OnModuleInit } from '@nestjs/common';
|
||||
import { HttpAdapterHost } from '@nestjs/core';
|
||||
import { join } from 'path';
|
||||
import * as fs from 'node:fs';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import { EnvironmentService } from '../environment/environment.service';
|
||||
|
||||
@Module({})
|
||||
export class StaticModule implements OnModuleInit {
|
||||
constructor(
|
||||
private readonly httpAdapterHost: HttpAdapterHost,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
public async onModuleInit() {
|
||||
const httpAdapter = this.httpAdapterHost.httpAdapter;
|
||||
const app = httpAdapter.getInstance();
|
||||
|
||||
const clientDistPath = join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'client/dist',
|
||||
);
|
||||
|
||||
if (fs.existsSync(clientDistPath)) {
|
||||
const indexFilePath = join(clientDistPath, 'index.html');
|
||||
const windowVar = '<!--window-config-->';
|
||||
|
||||
const configString = {
|
||||
env: this.environmentService.getEnv(),
|
||||
appUrl: this.environmentService.getAppUrl(),
|
||||
isCloud: this.environmentService.isCloud(),
|
||||
};
|
||||
|
||||
const windowScriptContent = `<script>window.CONFIG=${JSON.stringify(configString)};</script>`;
|
||||
const html = fs.readFileSync(indexFilePath, 'utf8');
|
||||
const transformedHtml = html.replace(windowVar, windowScriptContent);
|
||||
|
||||
fs.writeFileSync(indexFilePath, transformedHtml);
|
||||
|
||||
const RENDER_PATH = '*';
|
||||
|
||||
await app.register(fastifyStatic, {
|
||||
root: clientDistPath,
|
||||
wildcard: false,
|
||||
});
|
||||
|
||||
app.get(RENDER_PATH, (req: any, res: any) => {
|
||||
const stream = fs.createReadStream(indexFilePath);
|
||||
res.type('text/html').send(stream);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import { type Kysely } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.alterTable('pages')
|
||||
.addColumn('position', 'varchar', (col) => col)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.alterTable('pages').dropColumn('position').execute();
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.alterTable('workspace_invitations')
|
||||
.addColumn('token', 'varchar', (col) => col)
|
||||
.addColumn('group_ids', sql`uuid[]`, (col) => col)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('workspace_invitations')
|
||||
.dropColumn('status')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('workspace_invitations')
|
||||
.addUniqueConstraint('invitation_email_workspace_id_unique', [
|
||||
'email',
|
||||
'workspace_id',
|
||||
])
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.alterTable('workspace_invitations')
|
||||
.dropColumn('token')
|
||||
.execute();
|
||||
await db.schema
|
||||
.alterTable('workspace_invitations')
|
||||
.dropColumn('group_ids')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('workspace_invitations')
|
||||
.addColumn('status', 'varchar', (col) => col)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.alterTable('workspace_invitations')
|
||||
.dropConstraint('invitation_email_workspace_id_unique')
|
||||
.execute();
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import { type Kysely } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.alterTable('users')
|
||||
.addColumn('invited_by_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.alterTable('users').dropColumn('invited_by_id').execute();
|
||||
}
|
||||
@ -4,7 +4,7 @@ import {
|
||||
FastifyAdapter,
|
||||
NestFastifyApplication,
|
||||
} from '@nestjs/platform-fastify';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { NotFoundException, ValidationPipe } from '@nestjs/common';
|
||||
import { TransformHttpResponseInterceptor } from './interceptors/http-response.interceptor';
|
||||
import fastifyMultipart from '@fastify/multipart';
|
||||
|
||||
@ -14,12 +14,29 @@ async function bootstrap() {
|
||||
new FastifyAdapter({
|
||||
ignoreTrailingSlash: true,
|
||||
ignoreDuplicateSlashes: true,
|
||||
} as any),
|
||||
}),
|
||||
);
|
||||
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
await app.register(fastifyMultipart as any);
|
||||
await app.register(fastifyMultipart);
|
||||
|
||||
app
|
||||
.getHttpAdapter()
|
||||
.getInstance()
|
||||
.addHook('preHandler', function (req, reply, done) {
|
||||
if (
|
||||
req.originalUrl.startsWith('/api') &&
|
||||
!req.originalUrl.startsWith('/api/auth/setup')
|
||||
) {
|
||||
if (!req.raw?.['workspaceId']) {
|
||||
throw new NotFoundException('Workspace not found');
|
||||
}
|
||||
done();
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
|
||||
@ -17,7 +17,9 @@ export class DomainMiddleware implements NestMiddleware {
|
||||
if (this.environmentService.isSelfHosted()) {
|
||||
const workspace = await this.workspaceRepo.findFirst();
|
||||
if (!workspace) {
|
||||
throw new NotFoundException('Workspace not found');
|
||||
//throw new NotFoundException('Workspace not found');
|
||||
(req as any).workspaceId = null;
|
||||
return next();
|
||||
}
|
||||
|
||||
(req as any).workspaceId = workspace.id;
|
||||
|
||||
@ -20,8 +20,7 @@
|
||||
"strict": true,
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"@docmost/db": ["./src/kysely"],
|
||||
"@docmost/db/*": ["./src/kysely/*"],
|
||||
"@docmost/db/*": ["./src/database/*"],
|
||||
"@docmost/transactional/*": ["./src/integrations/transactional/*"]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user