mirror of
https://github.com/docmost/docmost.git
synced 2025-11-17 04:41:08 +10:00
collaborative editor - wip
This commit is contained in:
@ -5,6 +5,7 @@ import { CoreModule } from './core/core.module';
|
||||
import { EnvironmentModule } from './environment/environment.module';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AppDataSource } from './database/typeorm.config';
|
||||
import { CollaborationModule } from './collaboration/collaboration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -16,6 +17,7 @@ import { AppDataSource } from './database/typeorm.config';
|
||||
migrations: ['dist/src/**/migrations/*.{ts,js}'],
|
||||
autoLoadEntities: true,
|
||||
}),
|
||||
CollaborationModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
31
server/src/collaboration/collaboration.gateway.ts
Normal file
31
server/src/collaboration/collaboration.gateway.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import {
|
||||
OnGatewayConnection,
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { Server as HocuspocusServer } from '@hocuspocus/server';
|
||||
import { IncomingMessage } from 'http';
|
||||
import WebSocket, { Server } from 'ws';
|
||||
import { AuthenticationExtension } from './extensions/authentication.extension';
|
||||
import { PersistenceExtension } from './extensions/persistence.extension';
|
||||
|
||||
@WebSocketGateway({ path: '/collaboration' })
|
||||
export class CollaborationGateway implements OnGatewayConnection {
|
||||
@WebSocketServer()
|
||||
server: Server;
|
||||
|
||||
constructor(
|
||||
private authenticationExtension: AuthenticationExtension,
|
||||
private persistenceExtension: PersistenceExtension,
|
||||
) {}
|
||||
|
||||
private hocuspocus = HocuspocusServer.configure({
|
||||
debounce: 5000,
|
||||
maxDebounce: 10000,
|
||||
extensions: [this.authenticationExtension, this.persistenceExtension],
|
||||
});
|
||||
|
||||
handleConnection(client: WebSocket, request: IncomingMessage): any {
|
||||
this.hocuspocus.handleConnection(client, request);
|
||||
}
|
||||
}
|
||||
17
server/src/collaboration/collaboration.module.ts
Normal file
17
server/src/collaboration/collaboration.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UserModule } from '../core/user/user.module';
|
||||
import { AuthModule } from '../core/auth/auth.module';
|
||||
import { CollaborationGateway } from './collaboration.gateway';
|
||||
import { AuthenticationExtension } from './extensions/authentication.extension';
|
||||
import { PersistenceExtension } from './extensions/persistence.extension';
|
||||
import { PageModule } from '../core/page/page.module';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
CollaborationGateway,
|
||||
AuthenticationExtension,
|
||||
PersistenceExtension,
|
||||
],
|
||||
imports: [UserModule, AuthModule, PageModule],
|
||||
})
|
||||
export class CollaborationModule {}
|
||||
@ -0,0 +1,34 @@
|
||||
import { Extension, onAuthenticatePayload } from '@hocuspocus/server';
|
||||
import { UserService } from '../../core/user/user.service';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { TokenService } from '../../core/auth/services/token.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthenticationExtension implements Extension {
|
||||
constructor(
|
||||
private tokenService: TokenService,
|
||||
private userService: UserService,
|
||||
) {}
|
||||
|
||||
async onAuthenticate(data: onAuthenticatePayload) {
|
||||
const { documentName, token } = data;
|
||||
|
||||
let jwtPayload = null;
|
||||
|
||||
try {
|
||||
jwtPayload = await this.tokenService.verifyJwt(token);
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Could not verify jwt token');
|
||||
}
|
||||
|
||||
const userId = jwtPayload.sub;
|
||||
const user = await this.userService.findById(userId);
|
||||
|
||||
//TODO: Check if the page exists and verify user permissions for page.
|
||||
// if all fails, abort connection
|
||||
|
||||
return {
|
||||
user,
|
||||
};
|
||||
}
|
||||
}
|
||||
59
server/src/collaboration/extensions/persistence.extension.ts
Normal file
59
server/src/collaboration/extensions/persistence.extension.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Extension, onLoadDocumentPayload, onStoreDocumentPayload } from '@hocuspocus/server';
|
||||
import * as Y from 'yjs';
|
||||
import { PageService } from '../../core/page/page.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TiptapTransformer } from '@hocuspocus/transformer';
|
||||
|
||||
@Injectable()
|
||||
export class PersistenceExtension implements Extension {
|
||||
constructor(private readonly pageService: PageService) {}
|
||||
|
||||
async onLoadDocument(data: onLoadDocumentPayload) {
|
||||
const { documentName, document } = data;
|
||||
|
||||
if (!document.isEmpty('default')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const page = await this.pageService.findById(documentName);
|
||||
|
||||
if (!page) {
|
||||
console.log('page does not exist.');
|
||||
//TODO: terminate connection if the page does not exist?
|
||||
return;
|
||||
}
|
||||
|
||||
if (page.ydoc) {
|
||||
const doc = new Y.Doc();
|
||||
const dbState = new Uint8Array(page.ydoc);
|
||||
|
||||
Y.applyUpdate(doc, dbState);
|
||||
return doc;
|
||||
}
|
||||
|
||||
// if no ydoc state in db convert json in page.content to Ydoc.
|
||||
const ydoc = TiptapTransformer.toYdoc(page.content, 'default');
|
||||
|
||||
Y.encodeStateAsUpdate(ydoc);
|
||||
return ydoc;
|
||||
}
|
||||
|
||||
async onStoreDocument(data: onStoreDocumentPayload) {
|
||||
const { documentName, document, context } = data;
|
||||
|
||||
const pageId = documentName;
|
||||
|
||||
const tiptapJson = TiptapTransformer.fromYdoc(document, 'default');
|
||||
const ydocState = Buffer.from(Y.encodeStateAsUpdate(document));
|
||||
|
||||
try {
|
||||
await this.pageService.updateState(
|
||||
pageId,
|
||||
tiptapJson,
|
||||
ydocState,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`Failed to update page ${documentName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import { EnvironmentService } from '../../../environment/environment.service';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { TokensDto } from "../dto/tokens.dto";
|
||||
import { TokensDto } from '../dto/tokens.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TokenService {
|
||||
|
||||
@ -20,14 +20,14 @@ export class Page {
|
||||
@Column({ length: 500, nullable: true })
|
||||
title: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
content: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
html: string;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
json: any;
|
||||
@Column({ type: 'bytea', nullable: true })
|
||||
ydoc: any;
|
||||
|
||||
@Column({ nullable: true })
|
||||
slug: string;
|
||||
|
||||
@ -11,5 +11,6 @@ import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
imports: [TypeOrmModule.forFeature([Page]), AuthModule, WorkspaceModule],
|
||||
controllers: [PageController],
|
||||
providers: [PageService, PageRepository],
|
||||
exports: [PageService, PageRepository],
|
||||
})
|
||||
export class PageModule {}
|
||||
|
||||
@ -22,19 +22,29 @@ export class PageService {
|
||||
page.creatorId = userId;
|
||||
page.workspaceId = workspaceId;
|
||||
|
||||
console.log(page);
|
||||
return await this.pageRepository.save(page);
|
||||
}
|
||||
|
||||
async update(pageId: string, updatePageDto: UpdatePageDto): Promise<Page> {
|
||||
const existingPage = await this.pageRepository.findById(pageId);
|
||||
if (!existingPage) {
|
||||
throw new Error(`Page with ID ${pageId} not found`);
|
||||
}
|
||||
|
||||
const page = await this.pageRepository.preload({
|
||||
id: pageId,
|
||||
...updatePageDto,
|
||||
} as Page);
|
||||
|
||||
return await this.pageRepository.save(page);
|
||||
}
|
||||
|
||||
async updateState(pageId: string, content: any, ydoc: any): Promise<void> {
|
||||
await this.pageRepository.update(pageId, {
|
||||
content: content,
|
||||
ydoc: ydoc,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(pageId: string): Promise<void> {
|
||||
await this.pageRepository.softDelete(pageId);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
} from '@nestjs/platform-fastify';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { TransformHttpResponseInterceptor } from './interceptors/http-response.interceptor';
|
||||
import { WsAdapter } from '@nestjs/platform-ws';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
@ -25,6 +26,7 @@ async function bootstrap() {
|
||||
app.enableCors();
|
||||
|
||||
app.useGlobalInterceptors(new TransformHttpResponseInterceptor());
|
||||
app.useWebSocketAdapter(new WsAdapter(app));
|
||||
|
||||
await app.listen(process.env.PORT || 3001);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user