catalog-backend: async response writing + tests and fixes

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-12-12 13:39:48 +01:00
parent 6f09b9af99
commit daee494dfa
3 changed files with 177 additions and 17 deletions
@@ -171,7 +171,7 @@ export async function createRouter(
res.setHeader('link', `<${url.pathname}${url.search}>; rel="next"`);
}
writeEntitiesResponse(res, entities);
await writeEntitiesResponse(res, entities);
return;
}
@@ -253,7 +253,7 @@ export async function createRouter(
credentials: await httpAuth.credentials(req),
});
writeEntitiesResponse(res, items, entities => ({
await writeEntitiesResponse(res, items, entities => ({
items: entities,
totalItems,
pageInfo: {
@@ -312,7 +312,9 @@ export async function createRouter(
fields: parseEntityTransformParams(req.query, request.fields),
credentials: await httpAuth.credentials(req),
});
writeEntitiesResponse(res, items, entities => ({ items: entities }));
await writeEntitiesResponse(res, items, entities => ({
items: entities,
}));
})
.get('/entity-facets', async (req, res) => {
const response = await entitiesCatalog.facets({
@@ -17,7 +17,7 @@
import express from 'express';
import { mockErrorHandler } from '@backstage/backend-test-utils';
import request from 'supertest';
import { writeSingleEntityResponse } from './write';
import { writeEntitiesResponse, writeSingleEntityResponse } from './write';
describe('writeSingleEntityResponse', () => {
const app = express();
@@ -123,3 +123,159 @@ describe('writeSingleEntityResponse', () => {
});
});
});
describe('writeEntitiesResponse', () => {
const app = express();
app.use(express.json());
app.get('/echo', (req, res) => {
writeEntitiesResponse(res, req.body);
});
app.get('/wrapped', (req, res) => {
writeEntitiesResponse(res, req.body, entities => ({
page: 1,
items: entities,
totalItems: 1337,
}));
});
app.use(mockErrorHandler());
describe('in object form', () => {
it('should return empty list', async () => {
const res = await request(app).get('/echo').send({
type: 'object',
entities: [],
});
expect(res.status).toBe(200);
expect(res.type).toBe('application/json');
expect(res.header['content-type']).toBe(
'application/json; charset=utf-8',
);
expect(res.body).toEqual([]);
});
it('should return mixed objects', async () => {
const res = await request(app)
.get('/echo')
.send({
type: 'object',
entities: [{ kind: 'Component' }, null, { kind: 'User' }, null],
});
expect(res.status).toBe(200);
expect(res.type).toBe('application/json');
expect(res.header['content-type']).toBe(
'application/json; charset=utf-8',
);
expect(res.body).toEqual([
{ kind: 'Component' },
null,
{ kind: 'User' },
null,
]);
});
it('should wrap response of empty list', async () => {
const res = await request(app)
.get('/wrapped')
.send({ type: 'object', entities: [] });
expect(res.status).toBe(200);
expect(res.type).toBe('application/json');
expect(res.header['content-type']).toBe(
'application/json; charset=utf-8',
);
expect(res.body).toEqual({ page: 1, items: [], totalItems: 1337 });
});
it('should wrap response of mixed list', async () => {
const res = await request(app)
.get('/wrapped')
.send({
type: 'object',
entities: [{ kind: 'Component' }, null, { kind: 'User' }, null],
});
expect(res.status).toBe(200);
expect(res.type).toBe('application/json');
expect(res.header['content-type']).toBe(
'application/json; charset=utf-8',
);
expect(res.body).toEqual({
page: 1,
items: [{ kind: 'Component' }, null, { kind: 'User' }, null],
totalItems: 1337,
});
});
});
describe('in raw form', () => {
it('should return empty list', async () => {
const res = await request(app).get('/echo').send({
type: 'raw',
entities: [],
});
expect(res.status).toBe(200);
expect(res.type).toBe('application/json');
expect(res.header['content-type']).toBe(
'application/json; charset=utf-8',
);
expect(res.body).toEqual([]);
});
it('should return mixed objects', async () => {
const res = await request(app)
.get('/echo')
.send({
type: 'raw',
entities: ['{"kind":"Component"}', null, '{"kind":"User"}', null],
});
expect(res.status).toBe(200);
expect(res.type).toBe('application/json');
expect(res.header['content-type']).toBe(
'application/json; charset=utf-8',
);
expect(res.body).toEqual([
{ kind: 'Component' },
null,
{ kind: 'User' },
null,
]);
});
it('should wrap response of empty list', async () => {
const res = await request(app)
.get('/wrapped')
.send({ type: 'raw', entities: [] });
expect(res.status).toBe(200);
expect(res.type).toBe('application/json');
expect(res.header['content-type']).toBe(
'application/json; charset=utf-8',
);
expect(res.body).toEqual({ page: 1, items: [], totalItems: 1337 });
});
it('should wrap response of mixed list', async () => {
const res = await request(app)
.get('/wrapped')
.send({
type: 'raw',
entities: ['{"kind":"Component"}', null, '{"kind":"User"}', null],
});
expect(res.status).toBe(200);
expect(res.type).toBe('application/json');
expect(res.header['content-type']).toBe(
'application/json; charset=utf-8',
);
expect(res.body).toEqual({
page: 1,
items: [{ kind: 'Component' }, null, { kind: 'User' }, null],
totalItems: 1337,
});
});
});
});
@@ -42,7 +42,7 @@ export function writeSingleEntityResponse(
}
}
export function writeEntitiesResponse(
export async function writeEntitiesResponse(
res: Response,
response: EntitiesResponseItems,
responseWrapper?: (entities: JsonValue) => JsonValue,
@@ -63,7 +63,7 @@ export function writeEntitiesResponse(
if (responseWrapper) {
const marker = `__MARKER_${Math.random().toString(36).slice(2, 10)}__`;
const wrapped = JSON.stringify(responseWrapper(marker));
const parts = wrapped.split(marker);
const parts = wrapped.split(`"${marker}"`);
if (parts.length !== 2) {
throw new Error(
`Entity items response was incorrectly wrapped into ${parts.length} different parts`,
@@ -75,16 +75,18 @@ export function writeEntitiesResponse(
let first = true;
for (const entity of response.entities) {
if (first) {
res.write('[', 'utf8');
first = false;
} else {
res.write(',', 'utf8');
}
res.write(entity, 'utf8');
}
res.end(']');
if (trailing) {
res.write(trailing, 'utf8');
const prefix = first ? '[' : ',';
first = false;
await new Promise((resolve, reject) => {
res.write(prefix + entity, 'utf8', err => {
if (err) {
reject(err);
} else {
resolve(err);
}
});
});
}
res.end(`${first ? '[' : ''}]${trailing}`);
}