diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 7208c29a89..f89ae745ce 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -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({ diff --git a/plugins/catalog-backend/src/service/response/write.test.ts b/plugins/catalog-backend/src/service/response/write.test.ts index 412aefe404..b1a688b48d 100644 --- a/plugins/catalog-backend/src/service/response/write.test.ts +++ b/plugins/catalog-backend/src/service/response/write.test.ts @@ -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, + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/service/response/write.ts b/plugins/catalog-backend/src/service/response/write.ts index 1dac8d3ce4..950dd99ba7 100644 --- a/plugins/catalog-backend/src/service/response/write.ts +++ b/plugins/catalog-backend/src/service/response/write.ts @@ -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}`); }