Skip to content

Commit 5014443

Browse files
committed
💩(y-provider) init a markdown converter endpoint
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.
1 parent 3fef759 commit 5014443

File tree

7 files changed

+460
-382
lines changed

7 files changed

+460
-382
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to
2020

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

2425
## Changed
2526

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = {};

src/frontend/servers/y-provider/__tests__/server.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,39 @@ describe('Server Tests', () => {
9191
hocuspocusServer.closeConnections = closeConnections;
9292
});
9393

94+
test('POST /api/convert-markdown with incorrect API key should return 403', async () => {
95+
const response = await request(app as any)
96+
.post('/api/convert-markdown')
97+
.set('Origin', origin)
98+
.set('Authorization', 'wrong-api-key');
99+
100+
expect(response.status).toBe(403);
101+
expect(response.body.error).toBe('Forbidden: Invalid API Key');
102+
});
103+
104+
test('POST /api/convert-markdown with missing body param content', async () => {
105+
const response = await request(app as any)
106+
.post('/api/convert-markdown')
107+
.set('Origin', origin)
108+
.set('Authorization', 'test-secret-api-key');
109+
110+
expect(response.status).toBe(400);
111+
expect(response.body.error).toBe('Invalid request: missing content');
112+
});
113+
114+
test('POST /api/convert-markdown with body param content being an empty string', async () => {
115+
const response = await request(app as any)
116+
.post('/api/convert-markdown')
117+
.set('Origin', origin)
118+
.set('Authorization', 'test-secret-api-key')
119+
.send({
120+
content: '',
121+
});
122+
123+
expect(response.status).toBe(400);
124+
expect(response.body.error).toBe('Invalid request: missing content');
125+
});
126+
94127
['/collaboration/api/anything/', '/', '/anything'].forEach((path) => {
95128
test(`"${path}" endpoint should be forbidden`, async () => {
96129
const response = await request(app as any).post(path);

src/frontend/servers/y-provider/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ var config = {
66
},
77
moduleNameMapper: {
88
'^@/(.*)$': '<rootDir>/../src/$1',
9+
'^@blocknote/server-util$': '<rootDir>/../__mocks__/mock.js',
910
},
1011
};
1112
export default config;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const routes = {
22
COLLABORATION_WS: '/collaboration/ws/',
33
COLLABORATION_RESET_CONNECTIONS: '/collaboration/api/reset-connections/',
4+
CONVERT_MARKDOWN: '/api/convert-markdown/',
45
};

src/frontend/servers/y-provider/src/server.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
// eslint-disable-next-line import/order
22
import './services/sentry';
3+
import { ServerBlockNoteEditor } from '@blocknote/server-util';
34
import { Server } from '@hocuspocus/server';
45
import * as Sentry from '@sentry/node';
56
import express, { Request, Response } from 'express';
67
import expressWebsockets from 'express-ws';
8+
import * as Y from 'yjs';
79

810
import { PORT } from './env';
911
import { httpSecurity, wsSecurity } from './middlewares';
1012
import { routes } from './routes';
11-
import { logger } from './utils';
13+
import { logger, toBase64 } from './utils';
1214

1315
export const hocuspocusServer = Server.configure({
1416
name: 'docs-y-server',
@@ -133,6 +135,63 @@ export const initServer = () => {
133135
},
134136
);
135137

138+
interface ConversionRequest {
139+
content: string;
140+
}
141+
142+
interface ConversionResponse {
143+
content: string;
144+
}
145+
146+
interface ErrorResponse {
147+
error: string;
148+
}
149+
150+
/**
151+
* Route to convert markdown
152+
*/
153+
app.post(
154+
routes.CONVERT_MARKDOWN,
155+
httpSecurity,
156+
async (
157+
req: Request<
158+
object,
159+
ConversionResponse | ErrorResponse,
160+
ConversionRequest,
161+
object
162+
>,
163+
res: Response<ConversionResponse | ErrorResponse>,
164+
) => {
165+
const content = req.body?.content;
166+
167+
if (!content) {
168+
res.status(400).json({ error: 'Invalid request: missing content' });
169+
return;
170+
}
171+
172+
try {
173+
const editor = ServerBlockNoteEditor.create();
174+
175+
// Perform the conversion from markdown to Blocknote.js blocks
176+
const blocks = await editor.tryParseMarkdownToBlocks(content);
177+
178+
if (!blocks || blocks.length === 0) {
179+
res.status(500).json({ error: 'No valid blocks were generated' });
180+
return;
181+
}
182+
183+
// Create a Yjs Document from blocks, and encode it as a base64 string
184+
const yDocument = editor.blocksToYDoc(blocks, 'document-store');
185+
const documentContent = toBase64(Y.encodeStateAsUpdate(yDocument));
186+
187+
res.status(200).json({ content: documentContent });
188+
} catch (e) {
189+
logger('conversion failed:', e);
190+
res.status(500).json({ error: 'An error occurred' });
191+
}
192+
},
193+
);
194+
136195
Sentry.setupExpressErrorHandler(app);
137196

138197
app.get('/ping', (req, res) => {

0 commit comments

Comments
 (0)