Skip to content

Commit

Permalink
💩(y-provider) init a markdown converter endpoint
Browse files Browse the repository at this point in the history
This code is quite poor. Sorry, I don't have much time working
on this feature. However, it should be functional.

I've reused the code we created for the Demo with Kasbarian.
I've not tested it yet with all corner case. Error handling
might be improved for sure, same for logging.

This endpoint is not modular. We could easily introduce options
to modify its behavior based on some options. YAGNI

I've added bearer token authentification, because it's unclear
how this micro service would be exposed. It's totally not required
if the microservice is not exposed through an Ingress.
  • Loading branch information
lebaudantoine committed Dec 12, 2024
1 parent 471e98c commit bdafd87
Show file tree
Hide file tree
Showing 7 changed files with 460 additions and 382 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to

- ✨(backend) annotate number of accesses on documents in list view #429
- ✨(backend) allow users to mark/unmark documents as favorite #429
- ✨(y-provider) create a markdown converter endpoint #488

## Changed

Expand Down
1 change: 1 addition & 0 deletions src/frontend/servers/y-provider/__mocks__/mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
33 changes: 33 additions & 0 deletions src/frontend/servers/y-provider/__tests__/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,39 @@ describe('Server Tests', () => {
hocuspocusServer.closeConnections = closeConnections;
});

test('POST /api/convert-markdown with incorrect API key should return 403', async () => {
const response = await request(app as any)
.post('/api/convert-markdown')
.set('Origin', origin)
.set('Authorization', 'wrong-api-key');

expect(response.status).toBe(403);
expect(response.body.error).toBe('Forbidden: Invalid API Key');
});

test('POST /api/convert-markdown with missing body param content', async () => {
const response = await request(app as any)
.post('/api/convert-markdown')
.set('Origin', origin)
.set('Authorization', 'test-secret-api-key');

expect(response.status).toBe(400);
expect(response.body.error).toBe('Invalid request: missing content');
});

test('POST /api/convert-markdown with body param content being an empty string', async () => {
const response = await request(app as any)
.post('/api/convert-markdown')
.set('Origin', origin)
.set('Authorization', 'test-secret-api-key')
.send({
content: '',
});

expect(response.status).toBe(400);
expect(response.body.error).toBe('Invalid request: missing content');
});

['/collaboration/api/anything/', '/', '/anything'].forEach((path) => {
test(`"${path}" endpoint should be forbidden`, async () => {
const response = await request(app as any).post(path);
Expand Down
1 change: 1 addition & 0 deletions src/frontend/servers/y-provider/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var config = {
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/../src/$1',
'^@blocknote/server-util$': '<rootDir>/../__mocks__/mock.js',
},
};
export default config;
1 change: 1 addition & 0 deletions src/frontend/servers/y-provider/src/routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const routes = {
COLLABORATION_WS: '/collaboration/ws/',
COLLABORATION_RESET_CONNECTIONS: '/collaboration/api/reset-connections/',
CONVERT_MARKDOWN: '/api/convert-markdown/',
};
61 changes: 60 additions & 1 deletion src/frontend/servers/y-provider/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// eslint-disable-next-line import/order
import './services/sentry';
import { ServerBlockNoteEditor } from '@blocknote/server-util';
import { Server } from '@hocuspocus/server';
import * as Sentry from '@sentry/node';
import express, { Request, Response } from 'express';
import expressWebsockets from 'express-ws';
import * as Y from 'yjs';

import { PORT } from './env';
import { httpSecurity, wsSecurity } from './middlewares';
import { routes } from './routes';
import { logger } from './utils';
import { logger, toBase64 } from './utils';

export const hocuspocusServer = Server.configure({
name: 'docs-y-server',
Expand Down Expand Up @@ -133,6 +135,63 @@ export const initServer = () => {
},
);

interface ConversionRequest {
content: string;
}

interface ConversionResponse {
content: string;
}

interface ErrorResponse {
error: string;
}

/**
* Route to convert markdown
*/
app.post(
routes.CONVERT_MARKDOWN,
httpSecurity,
async (
req: Request<
object,
ConversionResponse | ErrorResponse,
ConversionRequest,
object
>,
res: Response<ConversionResponse | ErrorResponse>,
) => {
const content = req.body?.content;

if (!content) {
res.status(400).json({ error: 'Invalid request: missing content' });
return;
}

try {
const editor = ServerBlockNoteEditor.create();

// Perform the conversion from markdown to Blocknote.js blocks
const blocks = await editor.tryParseMarkdownToBlocks(content);

if (!blocks || blocks.length === 0) {
res.status(500).json({ error: 'No valid blocks were generated' });
return;
}

// Create a Yjs Document from blocks, and encode it as a base64 string
const yDocument = editor.blocksToYDoc(blocks, 'document-store');
const documentContent = toBase64(Y.encodeStateAsUpdate(yDocument));

res.status(200).json({ content: documentContent });
} catch (e) {
logger('conversion failed:', e);
res.status(500).json({ error: 'An error occurred' });
}
},
);

Sentry.setupExpressErrorHandler(app);

app.get('/ping', (req, res) => {
Expand Down
Loading

0 comments on commit bdafd87

Please sign in to comment.