diff --git a/.changeset/chatty-crabs-tap.md b/.changeset/chatty-crabs-tap.md new file mode 100644 index 0000000000..97ec05fb61 --- /dev/null +++ b/.changeset/chatty-crabs-tap.md @@ -0,0 +1,18 @@ +--- +'@backstage/repo-tools': minor +--- + +Adds a lint rule to `repo schema openapi lint` to enforce `allowReserved` for all parameters. To fix this, simply add `allowReserved: true` to your parameters, like so + +```diff +/v1/todos: + get: + operationId: ListTodos + # ... + parameters: + - name: entity + in: query ++ allowReserved: true + schema: + type: string +``` diff --git a/.changeset/lucky-comics-hang.md b/.changeset/lucky-comics-hang.md new file mode 100644 index 0000000000..0174676d14 --- /dev/null +++ b/.changeset/lucky-comics-hang.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-backend': patch +'@backstage/plugin-todo-backend': patch +--- + +Allow reserved characters in requests. diff --git a/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts b/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts index eacb3ac50d..515d1d68ed 100644 --- a/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts +++ b/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts @@ -26,6 +26,7 @@ import fs from 'fs-extra'; import chalk from 'chalk'; import { runner } from '../../../../lib/runner'; import { oas } from '@stoplight/spectral-rulesets'; +import { truthy } from '@stoplight/spectral-functions'; import { DiagnosticSeverity } from '@stoplight/types'; import { pretty } from '@stoplight/spectral-formatters'; import { getPathToOpenApiSpec } from '../../../../lib/openapi/helpers'; @@ -47,6 +48,16 @@ async function lint(directoryPath: string, config?: { strict: boolean }) { const backstageRuleset = new Ruleset( { extends: [oas, ruleset], + rules: { + 'allow-reserved-in-params': { + given: '$.paths..parameters[*]', + then: { + field: 'allowReserved', + function: truthy, + }, + severity: 'error', + }, + }, overrides: [ { files: ['*'], @@ -63,8 +74,8 @@ async function lint(directoryPath: string, config?: { strict: boolean }) { } as RulesetDefinition, { source: openapiPath }, ); - spectral.setRuleset(backstageRuleset); + // we lint our document using the ruleset we passed to the Spectral object const result = await spectral.run(document); const errors = result.filter(e => e.severity === DiagnosticSeverity.Error); diff --git a/packages/repo-tools/src/lib/runner.ts b/packages/repo-tools/src/lib/runner.ts index b9700cd09b..ea7b511938 100644 --- a/packages/repo-tools/src/lib/runner.ts +++ b/packages/repo-tools/src/lib/runner.ts @@ -37,6 +37,7 @@ export async function runner( port: options.startingPort, stopPort: options.startingPort + 1_000, })); + const resultsList = await Promise.all( packages.map(pkg => limit(async () => { diff --git a/plugins/search-backend/src/schema/openapi.generated.ts b/plugins/search-backend/src/schema/openapi.generated.ts index 06b576ba70..8ad27546d6 100644 --- a/plugins/search-backend/src/schema/openapi.generated.ts +++ b/plugins/search-backend/src/schema/openapi.generated.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * Copyright 2024 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. @@ -197,6 +197,7 @@ export const spec = { name: 'term', in: 'query', required: false, + allowReserved: true, schema: { type: 'string', default: '', @@ -208,6 +209,7 @@ export const spec = { required: false, style: 'deepObject', explode: true, + allowReserved: true, schema: { $ref: '#/components/schemas/JsonObject', }, @@ -216,6 +218,7 @@ export const spec = { name: 'types', in: 'query', required: false, + allowReserved: true, schema: { type: 'array', items: { @@ -227,6 +230,7 @@ export const spec = { name: 'pageCursor', in: 'query', required: false, + allowReserved: true, schema: { type: 'string', }, @@ -235,6 +239,7 @@ export const spec = { name: 'pageLimit', in: 'query', required: false, + allowReserved: true, schema: { type: 'integer', }, diff --git a/plugins/search-backend/src/schema/openapi.yaml b/plugins/search-backend/src/schema/openapi.yaml index 47f7e0a1e5..358f94fcf6 100644 --- a/plugins/search-backend/src/schema/openapi.yaml +++ b/plugins/search-backend/src/schema/openapi.yaml @@ -132,6 +132,7 @@ paths: - name: term in: query required: false + allowReserved: true schema: type: string default: '' @@ -140,6 +141,7 @@ paths: required: false style: deepObject explode: true + allowReserved: 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 @@ -148,6 +150,7 @@ paths: - name: types in: query required: false + allowReserved: true schema: type: array items: @@ -155,10 +158,12 @@ paths: - name: pageCursor in: query required: false + allowReserved: true schema: type: string - name: pageLimit in: query required: false + allowReserved: true schema: type: integer diff --git a/plugins/search-backend/src/service/router.test.ts b/plugins/search-backend/src/service/router.test.ts index 7c89a8c750..59f2f95afe 100644 --- a/plugins/search-backend/src/service/router.test.ts +++ b/plugins/search-backend/src/service/router.test.ts @@ -99,7 +99,6 @@ describe('createRouter', () => { mockSearchEngine.query.mockRejectedValueOnce(error); const response = await request(app).get('/query'); - console.log((response as any).text); expect(response.status).toEqual(500); expect(response.body).toMatchObject( @@ -127,6 +126,8 @@ describe('createRouter', () => { 'types[0]=first-type&types[1]=second-type', 'filters[prop]=value', 'pageCursor=foo', + // https://github.com/backstage/backstage/issues/23973 + 'term=foo+bar', ])('accepts valid query string "%s"', async queryString => { const response = await request(app).get(`/query?${queryString}`); diff --git a/plugins/todo-backend/src/schema/openapi.generated.ts b/plugins/todo-backend/src/schema/openapi.generated.ts index 238be2fe5b..bbd9f1efb0 100644 --- a/plugins/todo-backend/src/schema/openapi.generated.ts +++ b/plugins/todo-backend/src/schema/openapi.generated.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * Copyright 2024 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. @@ -162,6 +162,7 @@ export const spec = { name: 'orderBy', in: 'query', required: false, + allowReserved: true, schema: { type: 'string', pattern: '^(text|tag|author|viewUrl|repoFilePath)=(asc|desc)$', @@ -188,6 +189,7 @@ export const spec = { name: 'offset', in: 'query', required: false, + allowReserved: true, schema: { type: 'integer', minimum: 0, @@ -198,6 +200,7 @@ export const spec = { name: 'limit', in: 'query', required: false, + allowReserved: true, schema: { type: 'integer', minimum: 1, diff --git a/plugins/todo-backend/src/schema/openapi.yaml b/plugins/todo-backend/src/schema/openapi.yaml index 7408927b99..98f92103fb 100644 --- a/plugins/todo-backend/src/schema/openapi.yaml +++ b/plugins/todo-backend/src/schema/openapi.yaml @@ -104,6 +104,7 @@ paths: - name: orderBy in: query required: false + allowReserved: true schema: type: string pattern: '^(text|tag|author|viewUrl|repoFilePath)=(asc|desc)$' @@ -121,6 +122,7 @@ paths: - name: offset in: query required: false + allowReserved: true schema: type: integer minimum: 0 @@ -128,6 +130,7 @@ paths: - name: limit in: query required: false + allowReserved: true schema: type: integer minimum: 1