Add an openapi spec to the search backend.
Signed-off-by: Aramis Sennyey <sennyeya@amazon.com>
This commit is contained in:
committed by
Renan Mendes Carvalho
parent
3b45c59744
commit
021cfbb515
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-openapi-utils': patch
|
||||
---
|
||||
|
||||
Corrected resolution of parameter nested schema to use central schemas.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend': patch
|
||||
---
|
||||
|
||||
Added an OpenAPI 3.0 spec and enforced schema-first model on the router.
|
||||
@@ -333,7 +333,6 @@ declare namespace internal {
|
||||
ImmutableSchemaObject,
|
||||
DocParameter,
|
||||
DocParameters,
|
||||
ResolveDocParameterSchema,
|
||||
ParameterSchema,
|
||||
MapToSchema,
|
||||
ParametersSchema,
|
||||
@@ -412,7 +411,7 @@ type OptionalMap<
|
||||
type ParameterSchema<
|
||||
Doc extends RequiredDoc,
|
||||
Schema extends ImmutableParameterObject['schema'],
|
||||
> = ResolveDocParameterSchema<Doc, Schema> extends infer R
|
||||
> = SchemaRef<Doc, Schema> extends infer R
|
||||
? R extends ImmutableSchemaObject
|
||||
? R extends JSONSchema7
|
||||
? FromSchema<R>
|
||||
@@ -549,16 +548,6 @@ type RequiredMap<
|
||||
[P in Exclude<PickRequiredKeys<T>, undefined>]: NonNullable<T[P]>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
type ResolveDocParameterSchema<
|
||||
Doc extends RequiredDoc,
|
||||
Schema extends ImmutableParameterObject['schema'],
|
||||
> = Schema extends ImmutableReferenceObject
|
||||
? 'parameters' extends ComponentTypes<Doc>
|
||||
? ComponentRef<Doc, 'parameters', Schema>
|
||||
: never
|
||||
: Schema;
|
||||
|
||||
// @public (undocumented)
|
||||
type Response_2<
|
||||
Doc extends RequiredDoc,
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
MapDiscriminatedUnion,
|
||||
PathTemplate,
|
||||
RequiredDoc,
|
||||
SchemaRef,
|
||||
} from './common';
|
||||
import { FromSchema, JSONSchema7 } from 'json-schema-to-ts';
|
||||
|
||||
@@ -76,25 +77,13 @@ export type DocParameters<
|
||||
}
|
||||
: never;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ResolveDocParameterSchema<
|
||||
Doc extends RequiredDoc,
|
||||
Schema extends ImmutableParameterObject['schema'],
|
||||
> = Schema extends ImmutableReferenceObject
|
||||
? 'parameters' extends ComponentTypes<Doc>
|
||||
? ComponentRef<Doc, 'parameters', Schema>
|
||||
: never
|
||||
: Schema;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ParameterSchema<
|
||||
Doc extends RequiredDoc,
|
||||
Schema extends ImmutableParameterObject['schema'],
|
||||
> = ResolveDocParameterSchema<Doc, Schema> extends infer R
|
||||
> = SchemaRef<Doc, Schema> extends infer R
|
||||
? R extends ImmutableSchemaObject
|
||||
? R extends JSONSchema7
|
||||
? FromSchema<R>
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-openapi-utils": "workspace:^",
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/supertest": "^2.0.8",
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
|
||||
export default {
|
||||
openapi: '3.0.3',
|
||||
info: {
|
||||
title: '@backstage/plugin-search-backend',
|
||||
version: '1',
|
||||
description:
|
||||
'The Backstage backend plugin that provides search functionality.',
|
||||
license: {
|
||||
name: 'Apache-2.0',
|
||||
url: 'http://www.apache.org/licenses/LICENSE-2.0.html',
|
||||
},
|
||||
contact: {},
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
components: {
|
||||
examples: {},
|
||||
headers: {},
|
||||
parameters: {},
|
||||
requestBodies: {},
|
||||
responses: {},
|
||||
schemas: {
|
||||
JsonObject: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
additionalProperties: {},
|
||||
},
|
||||
},
|
||||
securitySchemes: {
|
||||
JWT: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
},
|
||||
},
|
||||
},
|
||||
paths: {
|
||||
'/query': {
|
||||
get: {
|
||||
operationId: 'Query',
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Ok',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
results: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'The "type" of the given document.',
|
||||
},
|
||||
document: {
|
||||
type: 'object',
|
||||
description:
|
||||
'The raw value of the document, as indexed.',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The primary name of the document (e.g. name, title, identifier, etc).',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Free-form text of the document (e.g. description, content, etc).',
|
||||
},
|
||||
location: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The relative or absolute URL of the document (target when a search result is clicked).',
|
||||
},
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Optional result highlight. Useful for improving the search result\ndisplay/experience.',
|
||||
},
|
||||
rank: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'Optional result rank, where 1 is the first/top result returned. \nUseful for understanding search effectiveness in analytics.',
|
||||
},
|
||||
},
|
||||
required: ['type', 'document'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
nextPageCursor: {
|
||||
type: 'string',
|
||||
},
|
||||
previousPageCursor: {
|
||||
type: 'string',
|
||||
},
|
||||
numberOfResults: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
required: ['results'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'400': {
|
||||
description: 'Bad request',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
security: [
|
||||
{},
|
||||
{
|
||||
JWT: [],
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
name: 'term',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'filters',
|
||||
in: 'query',
|
||||
required: false,
|
||||
style: 'deepObject',
|
||||
explode: true,
|
||||
schema: {
|
||||
$ref: '#/components/schemas/JsonObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'types',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pageCursor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pageLimit',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -0,0 +1,135 @@
|
||||
openapi: 3.0.3
|
||||
|
||||
info:
|
||||
title: '@backstage/plugin-search-backend'
|
||||
version: '1'
|
||||
description: The Backstage backend plugin that provides search functionality.
|
||||
license:
|
||||
name: Apache-2.0
|
||||
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
contact: {}
|
||||
|
||||
servers:
|
||||
- url: /
|
||||
|
||||
components:
|
||||
examples: {}
|
||||
headers: {}
|
||||
parameters: {}
|
||||
requestBodies: {}
|
||||
responses: {}
|
||||
schemas:
|
||||
JsonObject:
|
||||
type: object
|
||||
properties: {}
|
||||
# Free form object.
|
||||
additionalProperties: {}
|
||||
securitySchemes:
|
||||
JWT:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
paths:
|
||||
/query:
|
||||
get:
|
||||
operationId: Query
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: The "type" of the given document.
|
||||
document:
|
||||
type: object
|
||||
description: The raw value of the document, as indexed.
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: The primary name of the document (e.g. name, title, identifier, etc).
|
||||
text:
|
||||
type: string
|
||||
description: Free-form text of the document (e.g. description, content, etc).
|
||||
location:
|
||||
type: string
|
||||
description: The relative or absolute URL of the document (target when a search result is clicked).
|
||||
highlight:
|
||||
type: object
|
||||
description: |-
|
||||
Optional result highlight. Useful for improving the search result
|
||||
display/experience.
|
||||
rank:
|
||||
type: integer
|
||||
description: |-
|
||||
Optional result rank, where 1 is the first/top result returned.
|
||||
Useful for understanding search effectiveness in analytics.
|
||||
required:
|
||||
- type
|
||||
- document
|
||||
additionalProperties: false
|
||||
nextPageCursor:
|
||||
type: string
|
||||
previousPageCursor:
|
||||
type: string
|
||||
numberOfResults:
|
||||
type: integer
|
||||
required:
|
||||
- results
|
||||
'400':
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
security:
|
||||
- {}
|
||||
- JWT: []
|
||||
parameters:
|
||||
- name: term
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
default: ''
|
||||
- name: filters
|
||||
in: query
|
||||
required: false
|
||||
style: deepObject
|
||||
explode: true
|
||||
schema:
|
||||
# JsonObject is used here instead of the full ZOD schema definition as
|
||||
# resolution of recursive schemas isn't possible with the library we're using
|
||||
# and it causes a performance hit when _trying_ to resolve them.
|
||||
$ref: '#/components/schemas/JsonObject'
|
||||
- name: types
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
- name: pageCursor
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: pageLimit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
@@ -19,7 +19,7 @@ import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import { z } from 'zod';
|
||||
import { errorHandler } from '@backstage/backend-common';
|
||||
import { ErrorResponseBody, InputError } from '@backstage/errors';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { Config } from '@backstage/config';
|
||||
import { JsonObject, JsonValue } from '@backstage/types';
|
||||
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
|
||||
@@ -35,6 +35,8 @@ import {
|
||||
} from '@backstage/plugin-search-common';
|
||||
import { SearchEngine } from '@backstage/plugin-search-common';
|
||||
import { AuthorizedSearchEngine } from './AuthorizedSearchEngine';
|
||||
import type { ApiRouter } from '@backstage/backend-openapi-utils';
|
||||
import spec from '../schema/openapi.generated';
|
||||
|
||||
const jsonObjectSchema: z.ZodSchema<JsonObject> = z.lazy(() => {
|
||||
const jsonValueSchema: z.ZodSchema<JsonValue> = z.lazy(() =>
|
||||
@@ -146,49 +148,43 @@ export async function createRouter(
|
||||
})),
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
router.get(
|
||||
'/query',
|
||||
async (
|
||||
req: express.Request,
|
||||
res: express.Response<SearchResultSet | ErrorResponseBody>,
|
||||
) => {
|
||||
const parseResult = requestSchema.passthrough().safeParse(req.query);
|
||||
const router = Router() as ApiRouter<typeof spec>;
|
||||
router.get('/query', async (req, res) => {
|
||||
const parseResult = requestSchema.passthrough().safeParse(req.query);
|
||||
|
||||
if (!parseResult.success) {
|
||||
throw new InputError(`Invalid query string: ${parseResult.error}`);
|
||||
if (!parseResult.success) {
|
||||
throw new InputError(`Invalid query string: ${parseResult.error}`);
|
||||
}
|
||||
|
||||
const query = parseResult.data;
|
||||
|
||||
logger.info(
|
||||
`Search request received: term="${query.term}", filters=${JSON.stringify(
|
||||
query.filters,
|
||||
)}, types=${query.types ? query.types.join(',') : ''}, pageCursor=${
|
||||
query.pageCursor ?? ''
|
||||
}`,
|
||||
);
|
||||
|
||||
const token = getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
);
|
||||
|
||||
try {
|
||||
const resultSet = await engine?.query(query, { token });
|
||||
|
||||
res.json(filterResultSet(toSearchResults(resultSet)));
|
||||
} catch (error) {
|
||||
if (error.name === 'MissingIndexError') {
|
||||
// re-throw and let the default error handler middleware captures it and serializes it with the right response code on the standard form
|
||||
throw error;
|
||||
}
|
||||
|
||||
const query = parseResult.data;
|
||||
|
||||
logger.info(
|
||||
`Search request received: term="${
|
||||
query.term
|
||||
}", filters=${JSON.stringify(query.filters)}, types=${
|
||||
query.types ? query.types.join(',') : ''
|
||||
}, pageCursor=${query.pageCursor ?? ''}`,
|
||||
throw new Error(
|
||||
`There was a problem performing the search query: ${error.message}`,
|
||||
);
|
||||
|
||||
const token = getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
);
|
||||
|
||||
try {
|
||||
const resultSet = await engine?.query(query, { token });
|
||||
|
||||
res.json(filterResultSet(toSearchResults(resultSet)));
|
||||
} catch (error) {
|
||||
if (error.name === 'MissingIndexError') {
|
||||
// re-throw and let the default error handler middleware captures it and serializes it with the right response code on the standard form
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`There was a problem performing the search query: ${error.message}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
router.use(errorHandler());
|
||||
|
||||
|
||||
@@ -8510,6 +8510,7 @@ __metadata:
|
||||
resolution: "@backstage/plugin-search-backend@workspace:plugins/search-backend"
|
||||
dependencies:
|
||||
"@backstage/backend-common": "workspace:^"
|
||||
"@backstage/backend-openapi-utils": "workspace:^"
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/backend-test-utils": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
|
||||
Reference in New Issue
Block a user