Merge pull request #28163 from backstage/rugvip/flag
catalog-backend: fully feature flag streamed raw json responses
This commit is contained in:
@@ -168,7 +168,11 @@ export async function createRouter(
|
||||
res.setHeader('link', `<${url.pathname}${url.search}>; rel="next"`);
|
||||
}
|
||||
|
||||
await writeEntitiesResponse(res, entities);
|
||||
await writeEntitiesResponse({
|
||||
res,
|
||||
items: entities,
|
||||
alwaysUseObjectMode: !disableRelationsCompatibility,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -214,18 +218,23 @@ export async function createRouter(
|
||||
credentials: await httpAuth.credentials(req),
|
||||
});
|
||||
|
||||
await writeEntitiesResponse(res, items, entities => ({
|
||||
items: entities,
|
||||
totalItems,
|
||||
pageInfo: {
|
||||
...(pageInfo.nextCursor && {
|
||||
nextCursor: encodeCursor(pageInfo.nextCursor),
|
||||
}),
|
||||
...(pageInfo.prevCursor && {
|
||||
prevCursor: encodeCursor(pageInfo.prevCursor),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
await writeEntitiesResponse({
|
||||
res,
|
||||
items,
|
||||
alwaysUseObjectMode: !disableRelationsCompatibility,
|
||||
responseWrapper: entities => ({
|
||||
items: entities,
|
||||
totalItems,
|
||||
pageInfo: {
|
||||
...(pageInfo.nextCursor && {
|
||||
nextCursor: encodeCursor(pageInfo.nextCursor),
|
||||
}),
|
||||
...(pageInfo.prevCursor && {
|
||||
prevCursor: encodeCursor(pageInfo.prevCursor),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
})
|
||||
.get('/entities/by-uid/:uid', async (req, res) => {
|
||||
const { uid } = req.params;
|
||||
@@ -273,9 +282,14 @@ export async function createRouter(
|
||||
fields: parseEntityTransformParams(req.query, request.fields),
|
||||
credentials: await httpAuth.credentials(req),
|
||||
});
|
||||
await writeEntitiesResponse(res, items, entities => ({
|
||||
items: entities,
|
||||
}));
|
||||
await writeEntitiesResponse({
|
||||
res,
|
||||
items,
|
||||
alwaysUseObjectMode: !disableRelationsCompatibility,
|
||||
responseWrapper: entities => ({
|
||||
items: entities,
|
||||
}),
|
||||
});
|
||||
})
|
||||
.get('/entity-facets', async (req, res) => {
|
||||
const response = await entitiesCatalog.facets({
|
||||
|
||||
@@ -128,14 +128,23 @@ describe('writeEntitiesResponse', () => {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.get('/echo', (req, res) => {
|
||||
writeEntitiesResponse(res, req.body);
|
||||
writeEntitiesResponse({
|
||||
res,
|
||||
items: req.body,
|
||||
alwaysUseObjectMode: Boolean(req.query.object),
|
||||
});
|
||||
});
|
||||
app.get('/wrapped', (req, res) => {
|
||||
writeEntitiesResponse(res, req.body, entities => ({
|
||||
page: 1,
|
||||
items: entities,
|
||||
totalItems: 1337,
|
||||
}));
|
||||
writeEntitiesResponse({
|
||||
res,
|
||||
items: req.body,
|
||||
alwaysUseObjectMode: Boolean(req.query.object),
|
||||
responseWrapper: entities => ({
|
||||
page: 1,
|
||||
items: entities,
|
||||
totalItems: 1337,
|
||||
}),
|
||||
});
|
||||
});
|
||||
app.use(mockErrorHandler());
|
||||
|
||||
@@ -151,6 +160,7 @@ describe('writeEntitiesResponse', () => {
|
||||
expect(res.header['content-type']).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(res.header['content-length']).toBeDefined();
|
||||
expect(res.body).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -167,6 +177,7 @@ describe('writeEntitiesResponse', () => {
|
||||
expect(res.header['content-type']).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(res.header['content-length']).toBeDefined();
|
||||
expect(res.body).toEqual([
|
||||
{ kind: 'Component' },
|
||||
null,
|
||||
@@ -185,6 +196,7 @@ describe('writeEntitiesResponse', () => {
|
||||
expect(res.header['content-type']).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(res.header['content-length']).toBeDefined();
|
||||
expect(res.body).toEqual({ page: 1, items: [], totalItems: 1337 });
|
||||
});
|
||||
|
||||
@@ -201,6 +213,7 @@ describe('writeEntitiesResponse', () => {
|
||||
expect(res.header['content-type']).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(res.header['content-length']).toBeDefined();
|
||||
expect(res.body).toEqual({
|
||||
page: 1,
|
||||
items: [{ kind: 'Component' }, null, { kind: 'User' }, null],
|
||||
@@ -221,6 +234,7 @@ describe('writeEntitiesResponse', () => {
|
||||
expect(res.header['content-type']).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(res.header['content-length']).toBeDefined();
|
||||
expect(res.body).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -237,6 +251,7 @@ describe('writeEntitiesResponse', () => {
|
||||
expect(res.header['content-type']).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(res.header['content-length']).not.toBeDefined();
|
||||
expect(res.body).toEqual([
|
||||
{ kind: 'Component' },
|
||||
null,
|
||||
@@ -255,6 +270,7 @@ describe('writeEntitiesResponse', () => {
|
||||
expect(res.header['content-type']).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(res.header['content-length']).not.toBeDefined();
|
||||
expect(res.body).toEqual({ page: 1, items: [], totalItems: 1337 });
|
||||
});
|
||||
|
||||
@@ -271,6 +287,7 @@ describe('writeEntitiesResponse', () => {
|
||||
expect(res.header['content-type']).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(res.header['content-length']).not.toBeDefined();
|
||||
expect(res.body).toEqual({
|
||||
page: 1,
|
||||
items: [{ kind: 'Component' }, null, { kind: 'User' }, null],
|
||||
@@ -307,6 +324,118 @@ describe('writeEntitiesResponse', () => {
|
||||
expect(res.header['content-type']).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(res.header['content-length']).not.toBeDefined();
|
||||
expect(res.body).toEqual({
|
||||
page: 1,
|
||||
items: expect.objectContaining({ length: 300 }),
|
||||
totalItems: 1337,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('in raw form forced to object', () => {
|
||||
it('should return empty list', async () => {
|
||||
const res = await request(app).get('/echo?object=true').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.header['content-length']).toBeDefined();
|
||||
expect(res.body).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return mixed objects', async () => {
|
||||
const res = await request(app)
|
||||
.get('/echo?object=true')
|
||||
.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.header['content-length']).toBeDefined();
|
||||
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?object=true')
|
||||
.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.header['content-length']).toBeDefined();
|
||||
expect(res.body).toEqual({ page: 1, items: [], totalItems: 1337 });
|
||||
});
|
||||
|
||||
it('should wrap response of mixed list', async () => {
|
||||
const res = await request(app)
|
||||
.get('/wrapped?object=true')
|
||||
.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.header['content-length']).toBeDefined();
|
||||
expect(res.body).toEqual({
|
||||
page: 1,
|
||||
items: [{ kind: 'Component' }, null, { kind: 'User' }, null],
|
||||
totalItems: 1337,
|
||||
});
|
||||
});
|
||||
|
||||
it('should write a large wrapped response', async () => {
|
||||
const entityMock = JSON.stringify({
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'my-component',
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': 'url:https://example.com',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
owner: 'me',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
});
|
||||
const res = await request(app)
|
||||
.get('/wrapped?object=true')
|
||||
.send({
|
||||
type: 'raw',
|
||||
entities: Array(300).fill(entityMock),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.type).toBe('application/json');
|
||||
expect(res.header['content-type']).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(res.header['content-length']).toBeDefined();
|
||||
expect(res.body).toEqual({
|
||||
page: 1,
|
||||
items: expect.objectContaining({ length: 300 }),
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Response } from 'express';
|
||||
import { EntitiesResponseItems } from '../../catalog/types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { processEntitiesResponseItems } from './process';
|
||||
|
||||
const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
|
||||
|
||||
@@ -42,16 +43,20 @@ export function writeSingleEntityResponse(
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeEntitiesResponse(
|
||||
res: Response,
|
||||
response: EntitiesResponseItems,
|
||||
responseWrapper?: (entities: JsonValue) => JsonValue,
|
||||
) {
|
||||
if (response.type === 'object') {
|
||||
export async function writeEntitiesResponse(options: {
|
||||
res: Response;
|
||||
items: EntitiesResponseItems;
|
||||
responseWrapper?: (entities: JsonValue) => JsonValue;
|
||||
alwaysUseObjectMode?: boolean;
|
||||
}) {
|
||||
const { res, responseWrapper, alwaysUseObjectMode } = options;
|
||||
const items = alwaysUseObjectMode
|
||||
? processEntitiesResponseItems(options.items, e => e)
|
||||
: options.items;
|
||||
|
||||
if (items.type === 'object') {
|
||||
res.json(
|
||||
responseWrapper
|
||||
? responseWrapper?.(response.entities)
|
||||
: response.entities,
|
||||
responseWrapper ? responseWrapper?.(items.entities) : items.entities,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -74,7 +79,7 @@ export async function writeEntitiesResponse(
|
||||
}
|
||||
|
||||
let first = true;
|
||||
for (const entity of response.entities) {
|
||||
for (const entity of items.entities) {
|
||||
const prefix = first ? '[' : ',';
|
||||
first = false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user