From 4669b87364d85f43955211a4d1cec9852221ce51 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 22 Feb 2023 17:00:48 -0500 Subject: [PATCH] Fix API definitions and update types. Signed-off-by: Aramis Sennyey --- plugins/catalog-backend/openapi.yaml | 46 +- .../src/service/schema/openapi.ts | 61 +- plugins/openapi-router-common/src/example.ts | 14 +- plugins/openapi-router-common/src/router.ts | 14 +- .../src/schema/openapi.ts | 1263 +++++++++++++++++ .../src/schema/petstore.ts | 2 + .../openapi-router-common/src/types/common.ts | 6 +- .../src/types/express.ts | 16 +- .../src/types/requests.ts | 10 +- .../src/types/response.ts | 9 +- 10 files changed, 1367 insertions(+), 74 deletions(-) create mode 100644 plugins/openapi-router-common/src/schema/openapi.ts diff --git a/plugins/catalog-backend/openapi.yaml b/plugins/catalog-backend/openapi.yaml index 5feacb8599..c53a6443ba 100644 --- a/plugins/catalog-backend/openapi.yaml +++ b/plugins/catalog-backend/openapi.yaml @@ -6,6 +6,7 @@ info: description: The Backstage backend plugin that provides the Backstage catalog license: name: Apache-2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html contact: {} servers: @@ -18,17 +19,6 @@ components: requestBodies: {} responses: {} schemas: - RefreshOptions: - type: object - properties: - authorizationToken: - type: string - entityRef: - type: string - description: The reference to a single entity that should be refreshed - required: - - entityRef - description: Options for requesting a refresh of entities in the catalog. JsonObject: type: object properties: {} @@ -57,6 +47,7 @@ components: required: - url description: A link to external information that is related to the entity. + additionalProperties: false EntityMeta: allOf: - $ref: '#/components/schemas/JsonObject' @@ -133,6 +124,7 @@ components: required: - name description: Metadata fields common to all versions/kinds of entity. + additionalProperties: false EntityRelation: type: object properties: @@ -146,6 +138,7 @@ components: - targetRef - type description: A relation of a specific type to another entity in the catalog. + additionalProperties: false Entity: type: object properties: @@ -171,6 +164,7 @@ components: - kind - apiVersion description: The parts of the format that's common to all versions/kinds of entity. + additionalProperties: false EntityAncestryResponse: type: object properties: @@ -193,6 +187,7 @@ components: required: - items - rootEntityRef + additionalProperties: false EntitiesBatchResponse: type: object properties: @@ -207,6 +202,7 @@ components: that are null signify that no entity existed with that ref. required: - items + additionalProperties: false EntityFacet: type: object properties: @@ -214,6 +210,7 @@ components: type: string count: type: number + additionalProperties: false EntityFacets: type: array items: @@ -227,6 +224,7 @@ components: $ref: '#/components/schemas/EntityFacets' required: - facets + additionalProperties: false Location: type: object properties: @@ -241,6 +239,7 @@ components: - type - id description: Entity location for a specific entity. + additionalProperties: false LocationSpec: type: object properties: @@ -257,6 +256,7 @@ components: - target - type description: Holds the entity location information. + additionalProperties: false AnalyzeLocationExistingEntity: type: object properties: @@ -275,6 +275,7 @@ components: read and emitted like this so that the frontend can inform the user that it located them and can make sure to register them as well if they weren't already + additionalProperties: false RecursivePartialEntityRelation: type: object properties: @@ -285,6 +286,7 @@ components: type: string description: The type of the relation. description: A relation of a specific type to another entity in the catalog. + additionalProperties: false RecursivePartialEntityMeta: allOf: - $ref: '#/components/schemas/JsonObject' @@ -360,6 +362,7 @@ components: update or delete operations, but the server is free to reject requests that do so in such a way that it breaks semantics. description: Metadata fields common to all versions/kinds of entity. + additionalProperties: false RecursivePartial_Entity_: type: object properties: @@ -381,6 +384,7 @@ components: $ref: '#/components/schemas/RecursivePartialEntityRelation' description: The relations that this entity has with other entities. description: Makes all keys of an entire hierarchy optional. + additionalProperties: false AnalyzeLocationEntityField: type: object properties: @@ -411,6 +415,7 @@ components: - value - state - field + additionalProperties: false AnalyzeLocationGenerateEntity: type: object properties: @@ -429,6 +434,7 @@ components: the frontend. It'll probably contain a (possibly incomplete) entity, plus enough info for the frontend to know what form data to show to the user for overriding/completing the info. + additionalProperties: false AnalyzeLocationResponse: type: object properties: @@ -443,6 +449,7 @@ components: required: - generateEntities - existingEntityFiles + additionalProperties: false LocationInput: type: object properties: @@ -459,6 +466,7 @@ components: - type - target - presence + additionalProperties: false SerializedError: allOf: - $ref: '#/components/schemas/JsonObject' @@ -480,13 +488,14 @@ components: - message - name description: The serialized form of an Error. + additionalProperties: false EntitiesQueryResponse: type: object properties: items: type: array items: - - $ref: '#/components/schemas/Entity' + $ref: '#/components/schemas/Entity' description: |- The list of entities paginated by a specific filter. totalItems: @@ -502,6 +511,7 @@ components: type: string description: |- Base64 encoded database query for the previous page. + additionalProperties: false securitySchemes: JWT: type: http @@ -523,7 +533,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RefreshOptions' + type: object + properties: + authorizationToken: + type: string + entityRef: + type: string + description: The reference to a single entity that should be refreshed + required: + - entityRef + description: Options for requesting a refresh of entities in the catalog. + additionalProperties: false /entities: get: operationId: GetEntities diff --git a/plugins/catalog-backend/src/service/schema/openapi.ts b/plugins/catalog-backend/src/service/schema/openapi.ts index a759d50c59..8d011ad7a4 100644 --- a/plugins/catalog-backend/src/service/schema/openapi.ts +++ b/plugins/catalog-backend/src/service/schema/openapi.ts @@ -22,6 +22,7 @@ export default { 'The Backstage backend plugin that provides the Backstage catalog', license: { name: 'Apache-2.0', + url: 'http://www.apache.org/licenses/LICENSE-2.0.html', }, contact: {}, }, @@ -37,22 +38,6 @@ export default { requestBodies: {}, responses: {}, schemas: { - RefreshOptions: { - type: 'object', - properties: { - authorizationToken: { - type: 'string', - }, - entityRef: { - type: 'string', - description: - 'The reference to a single entity that should be refreshed', - }, - }, - required: ['entityRef'], - description: - 'Options for requesting a refresh of entities in the catalog.', - }, JsonObject: { type: 'object', properties: {}, @@ -91,6 +76,7 @@ export default { required: ['url'], description: 'A link to external information that is related to the entity.', + additionalProperties: false, }, EntityMeta: { allOf: [ @@ -156,6 +142,7 @@ export default { }, ], description: 'Metadata fields common to all versions/kinds of entity.', + additionalProperties: false, }, EntityRelation: { type: 'object', @@ -172,6 +159,7 @@ export default { required: ['targetRef', 'type'], description: 'A relation of a specific type to another entity in the catalog.', + additionalProperties: false, }, Entity: { type: 'object', @@ -203,6 +191,7 @@ export default { required: ['metadata', 'kind', 'apiVersion'], description: "The parts of the format that's common to all versions/kinds of entity.", + additionalProperties: false, }, EntityAncestryResponse: { type: 'object', @@ -230,6 +219,7 @@ export default { }, }, required: ['items', 'rootEntityRef'], + additionalProperties: false, }, EntitiesBatchResponse: { type: 'object', @@ -249,6 +239,7 @@ export default { }, }, required: ['items'], + additionalProperties: false, }, EntityFacet: { type: 'object', @@ -260,6 +251,7 @@ export default { type: 'number', }, }, + additionalProperties: false, }, EntityFacets: { type: 'array', @@ -278,6 +270,7 @@ export default { }, }, required: ['facets'], + additionalProperties: false, }, Location: { type: 'object', @@ -294,6 +287,7 @@ export default { }, required: ['target', 'type', 'id'], description: 'Entity location for a specific entity.', + additionalProperties: false, }, LocationSpec: { type: 'object', @@ -311,6 +305,7 @@ export default { }, required: ['target', 'type'], description: 'Holds the entity location information.', + additionalProperties: false, }, AnalyzeLocationExistingEntity: { type: 'object', @@ -328,6 +323,7 @@ export default { required: ['entity', 'isRegistered', 'location'], description: "If the folder pointed to already contained catalog info yaml files, they are\nread and emitted like this so that the frontend can inform the user that it\nlocated them and can make sure to register them as well if they weren't\nalready", + additionalProperties: false, }, RecursivePartialEntityRelation: { type: 'object', @@ -343,6 +339,7 @@ export default { }, description: 'A relation of a specific type to another entity in the catalog.', + additionalProperties: false, }, RecursivePartialEntityMeta: { allOf: [ @@ -409,6 +406,7 @@ export default { 'Metadata fields common to all versions/kinds of entity.', }, ], + additionalProperties: false, }, RecursivePartial_Entity_: { type: 'object', @@ -438,6 +436,7 @@ export default { }, }, description: 'Makes all keys of an entire hierarchy optional.', + additionalProperties: false, }, AnalyzeLocationEntityField: { type: 'object', @@ -468,6 +467,7 @@ export default { }, }, required: ['description', 'value', 'state', 'field'], + additionalProperties: false, }, AnalyzeLocationGenerateEntity: { type: 'object', @@ -485,6 +485,7 @@ export default { required: ['fields', 'entity'], description: "This is some form of representation of what the analyzer could deduce.\nWe should probably have a chat about how this can best be conveyed to\nthe frontend. It'll probably contain a (possibly incomplete) entity, plus\nenough info for the frontend to know what form data to show to the user\nfor overriding/completing the info.", + additionalProperties: false, }, AnalyzeLocationResponse: { type: 'object', @@ -503,6 +504,7 @@ export default { }, }, required: ['generateEntities', 'existingEntityFiles'], + additionalProperties: false, }, LocationInput: { type: 'object', @@ -519,6 +521,7 @@ export default { }, }, required: ['type', 'target', 'presence'], + additionalProperties: false, }, SerializedError: { allOf: [ @@ -550,17 +553,16 @@ export default { }, ], description: 'The serialized form of an Error.', + additionalProperties: false, }, EntitiesQueryResponse: { type: 'object', properties: { items: { type: 'array', - items: [ - { - $ref: '#/components/schemas/Entity', - }, - ], + items: { + $ref: '#/components/schemas/Entity', + }, description: 'The list of entities paginated by a specific filter.', }, totalItems: { @@ -581,6 +583,7 @@ export default { }, }, }, + additionalProperties: false, }, }, securitySchemes: { @@ -612,7 +615,21 @@ export default { content: { 'application/json': { schema: { - $ref: '#/components/schemas/RefreshOptions', + type: 'object', + properties: { + authorizationToken: { + type: 'string', + }, + entityRef: { + type: 'string', + description: + 'The reference to a single entity that should be refreshed', + }, + }, + required: ['entityRef'], + description: + 'Options for requesting a refresh of entities in the catalog.', + additionalProperties: false, }, }, }, diff --git a/plugins/openapi-router-common/src/example.ts b/plugins/openapi-router-common/src/example.ts index d2dba3298a..f89fe17e3f 100644 --- a/plugins/openapi-router-common/src/example.ts +++ b/plugins/openapi-router-common/src/example.ts @@ -25,14 +25,22 @@ export async function createRouter( console.log(options); const router = Router() as ApiRouter>; - router.get('/pets/:uid', (req, res) => { + router.get('/pets/:uid', (_, res) => { res.json({ id: 1, - name: req.params.uid, + name: 'test', }); }); - // router.get('/pet') will complain with a TS error + router.get('/pets', (_, res) => { + res.json([ + { + id: 1, + tag: '123', + name: 'test', + }, + ]); + }); router.post('/pets', (_, res) => { res.send(); diff --git a/plugins/openapi-router-common/src/router.ts b/plugins/openapi-router-common/src/router.ts index 16b627e62e..68d0c8ecb1 100644 --- a/plugins/openapi-router-common/src/router.ts +++ b/plugins/openapi-router-common/src/router.ts @@ -23,17 +23,17 @@ export type DeepWriteable = { export interface ApiRouter extends Router { get: DocRequestMatcher; - post: DocRequestMatcher; + post: DocRequestMatcher; - all: DocRequestMatcher; + all: DocRequestMatcher; - put: DocRequestMatcher; + put: DocRequestMatcher; - delete: DocRequestMatcher; + delete: DocRequestMatcher; - patch: DocRequestMatcher; + patch: DocRequestMatcher; - options: DocRequestMatcher; + options: DocRequestMatcher; - head: DocRequestMatcher; + head: DocRequestMatcher; } diff --git a/plugins/openapi-router-common/src/schema/openapi.ts b/plugins/openapi-router-common/src/schema/openapi.ts new file mode 100644 index 0000000000..8d011ad7a4 --- /dev/null +++ b/plugins/openapi-router-common/src/schema/openapi.ts @@ -0,0 +1,1263 @@ +/* + * 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. + */ +export default { + openapi: '3.1.0', + info: { + title: '@backstage/plugin-catalog-backend', + version: '1', + description: + 'The Backstage backend plugin that provides the Backstage catalog', + 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: {}, + description: 'A type representing all allowed JSON object values.', + }, + MapStringString: { + type: 'object', + properties: {}, + additionalProperties: { + type: 'string', + }, + description: 'Construct a type with a set of properties K of type T', + }, + EntityLink: { + type: 'object', + properties: { + type: { + type: 'string', + description: + 'An optional value to categorize links into specific groups', + }, + icon: { + type: 'string', + description: + 'An optional semantic key that represents a visual icon.', + }, + title: { + type: 'string', + description: 'An optional descriptive title for the link.', + }, + url: { + type: 'string', + description: 'The url to the external site, document, etc.', + }, + }, + required: ['url'], + description: + 'A link to external information that is related to the entity.', + additionalProperties: false, + }, + EntityMeta: { + allOf: [ + { + $ref: '#/components/schemas/JsonObject', + }, + { + type: 'object', + properties: { + links: { + type: 'array', + items: { + $ref: '#/components/schemas/EntityLink', + }, + description: + 'A list of external hyperlinks related to the entity.', + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + description: + 'A list of single-valued strings, to for example classify catalog entities in\nvarious ways.', + }, + annotations: { + $ref: '#/components/schemas/MapStringString', + }, + labels: { + $ref: '#/components/schemas/MapStringString', + }, + description: { + type: 'string', + description: + 'A short (typically relatively few words, on one line) description of the\nentity.', + }, + title: { + type: 'string', + description: + 'A display name of the entity, to be presented in user interfaces instead\nof the `name` property above, when available.\nThis field is sometimes useful when the `name` is cumbersome or ends up\nbeing perceived as overly technical. The title generally does not have\nas stringent format requirements on it, so it may contain special\ncharacters and be more explanatory. Do keep it very short though, and\navoid situations where a title can be confused with the name of another\nentity, or where two entities share a title.\nNote that this is only for display purposes, and may be ignored by some\nparts of the code. Entity references still always make use of the `name`\nproperty, not the title.', + }, + namespace: { + type: 'string', + description: 'The namespace that the entity belongs to.', + }, + name: { + type: 'string', + description: + 'The name of the entity.\nMust be unique within the catalog at any given point in time, for any\ngiven namespace + kind pair. This value is part of the technical\nidentifier of the entity, and as such it will appear in URLs, database\ntables, entity references, and similar. It is subject to restrictions\nregarding what characters are allowed.\nIf you want to use a different, more human readable string with fewer\nrestrictions on it in user interfaces, see the `title` field below.', + }, + etag: { + type: 'string', + description: + 'An opaque string that changes for each update operation to any part of\nthe entity, including metadata.\nThis field can not be set by the user at creation time, and the server\nwill reject an attempt to do so. The field will be populated in read\noperations. The field can (optionally) be specified when performing\nupdate or delete operations, and the server will then reject the\noperation if it does not match the current stored value.', + }, + uid: { + type: 'string', + description: + 'A globally unique ID for the entity.\nThis field can not be set by the user at creation time, and the server\nwill reject an attempt to do so. The field will be populated in read\noperations. The field can (optionally) be specified when performing\nupdate or delete operations, but the server is free to reject requests\nthat do so in such a way that it breaks semantics.', + }, + }, + required: ['name'], + }, + ], + description: 'Metadata fields common to all versions/kinds of entity.', + additionalProperties: false, + }, + EntityRelation: { + type: 'object', + properties: { + targetRef: { + type: 'string', + description: 'The entity ref of the target of this relation.', + }, + type: { + type: 'string', + description: 'The type of the relation.', + }, + }, + required: ['targetRef', 'type'], + description: + 'A relation of a specific type to another entity in the catalog.', + additionalProperties: false, + }, + Entity: { + type: 'object', + properties: { + relations: { + type: 'array', + items: { + $ref: '#/components/schemas/EntityRelation', + }, + description: + 'The relations that this entity has with other entities.', + }, + spec: { + $ref: '#/components/schemas/JsonObject', + }, + metadata: { + $ref: '#/components/schemas/EntityMeta', + }, + kind: { + type: 'string', + description: 'The high level entity type being described.', + }, + apiVersion: { + type: 'string', + description: + 'The version of specification format for this particular entity that\nthis is written against.', + }, + }, + required: ['metadata', 'kind', 'apiVersion'], + description: + "The parts of the format that's common to all versions/kinds of entity.", + additionalProperties: false, + }, + EntityAncestryResponse: { + type: 'object', + properties: { + items: { + type: 'array', + items: { + type: 'object', + properties: { + parentEntityRefs: { + items: { + type: 'string', + }, + type: 'array', + }, + entity: { + $ref: '#/components/schemas/Entity', + }, + }, + required: ['parentEntityRefs', 'entity'], + }, + }, + rootEntityRef: { + type: 'string', + }, + }, + required: ['items', 'rootEntityRef'], + additionalProperties: false, + }, + EntitiesBatchResponse: { + type: 'object', + properties: { + items: { + type: 'array', + items: { + allOf: [ + { + $ref: '#/components/schemas/Entity', + }, + ], + nullable: true, + }, + description: + 'The list of entities, in the same order as the refs in the request. Entries\nthat are null signify that no entity existed with that ref.', + }, + }, + required: ['items'], + additionalProperties: false, + }, + EntityFacet: { + type: 'object', + properties: { + value: { + type: 'string', + }, + count: { + type: 'number', + }, + }, + additionalProperties: false, + }, + EntityFacets: { + type: 'array', + items: { + $ref: '#/components/schemas/EntityFacet', + }, + }, + EntityFacetsResponse: { + type: 'object', + properties: { + facets: { + type: 'object', + additionalProperties: { + $ref: '#/components/schemas/EntityFacets', + }, + }, + }, + required: ['facets'], + additionalProperties: false, + }, + Location: { + type: 'object', + properties: { + target: { + type: 'string', + }, + type: { + type: 'string', + }, + id: { + type: 'string', + }, + }, + required: ['target', 'type', 'id'], + description: 'Entity location for a specific entity.', + additionalProperties: false, + }, + LocationSpec: { + type: 'object', + properties: { + presence: { + type: 'string', + enum: ['optional', 'required'], + }, + target: { + type: 'string', + }, + type: { + type: 'string', + }, + }, + required: ['target', 'type'], + description: 'Holds the entity location information.', + additionalProperties: false, + }, + AnalyzeLocationExistingEntity: { + type: 'object', + properties: { + entity: { + $ref: '#/components/schemas/Entity', + }, + isRegistered: { + type: 'boolean', + }, + location: { + $ref: '#/components/schemas/LocationSpec', + }, + }, + required: ['entity', 'isRegistered', 'location'], + description: + "If the folder pointed to already contained catalog info yaml files, they are\nread and emitted like this so that the frontend can inform the user that it\nlocated them and can make sure to register them as well if they weren't\nalready", + additionalProperties: false, + }, + RecursivePartialEntityRelation: { + type: 'object', + properties: { + targetRef: { + type: 'string', + description: 'The entity ref of the target of this relation.', + }, + type: { + type: 'string', + description: 'The type of the relation.', + }, + }, + description: + 'A relation of a specific type to another entity in the catalog.', + additionalProperties: false, + }, + RecursivePartialEntityMeta: { + allOf: [ + { + $ref: '#/components/schemas/JsonObject', + }, + { + type: 'object', + properties: { + properties: null, + links: { + type: 'array', + items: { + $ref: '#/components/schemas/EntityLink', + }, + description: + 'A list of external hyperlinks related to the entity.', + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + description: + 'A list of single-valued strings, to for example classify catalog entities in\nvarious ways.', + }, + annotations: { + $ref: '#/components/schemas/MapStringString', + }, + labels: { + $ref: '#/components/schemas/MapStringString', + }, + description: { + type: 'string', + description: + 'A short (typically relatively few words, on one line) description of the\nentity.', + }, + title: { + type: 'string', + description: + 'A display name of the entity, to be presented in user interfaces instead\nof the `name` property above, when available.\nThis field is sometimes useful when the `name` is cumbersome or ends up\nbeing perceived as overly technical. The title generally does not have\nas stringent format requirements on it, so it may contain special\ncharacters and be more explanatory. Do keep it very short though, and\navoid situations where a title can be confused with the name of another\nentity, or where two entities share a title.\nNote that this is only for display purposes, and may be ignored by some\nparts of the code. Entity references still always make use of the `name`\nproperty, not the title.', + }, + namespace: { + type: 'string', + description: 'The namespace that the entity belongs to.', + }, + name: { + type: 'string', + description: + 'The name of the entity.\nMust be unique within the catalog at any given point in time, for any\ngiven namespace + kind pair. This value is part of the technical\nidentifier of the entity, and as such it will appear in URLs, database\ntables, entity references, and similar. It is subject to restrictions\nregarding what characters are allowed.\nIf you want to use a different, more human readable string with fewer\nrestrictions on it in user interfaces, see the `title` field below.', + }, + etag: { + type: 'string', + description: + 'An opaque string that changes for each update operation to any part of\nthe entity, including metadata.\nThis field can not be set by the user at creation time, and the server\nwill reject an attempt to do so. The field will be populated in read\noperations. The field can (optionally) be specified when performing\nupdate or delete operations, and the server will then reject the\noperation if it does not match the current stored value.', + }, + uid: { + type: 'string', + description: + 'A globally unique ID for the entity.\nThis field can not be set by the user at creation time, and the server\nwill reject an attempt to do so. The field will be populated in read\noperations. The field can (optionally) be specified when performing\nupdate or delete operations, but the server is free to reject requests\nthat do so in such a way that it breaks semantics.', + }, + }, + description: + 'Metadata fields common to all versions/kinds of entity.', + }, + ], + additionalProperties: false, + }, + RecursivePartial_Entity_: { + type: 'object', + properties: { + apiVersion: { + type: 'string', + description: + 'The version of specification format for this particular entity that\nthis is written against.', + }, + kind: { + type: 'string', + description: 'The high level entity type being described.', + }, + metadata: { + $ref: '#/components/schemas/RecursivePartialEntityMeta', + }, + spec: { + $ref: '#/components/schemas/JsonObject', + }, + relations: { + type: 'array', + items: { + $ref: '#/components/schemas/RecursivePartialEntityRelation', + }, + description: + 'The relations that this entity has with other entities.', + }, + }, + description: 'Makes all keys of an entire hierarchy optional.', + additionalProperties: false, + }, + AnalyzeLocationEntityField: { + type: 'object', + properties: { + description: { + type: 'string', + description: + 'A text to show to the user to inform about the choices made. Like, it could say\n"Found a CODEOWNERS file that covers this target, so we suggest leaving this \nfield empty; which would currently make it owned by X" where X is taken from the\ncodeowners file.', + }, + value: { + type: 'string', + nullable: true, + }, + state: { + type: 'string', + enum: [ + 'analysisSuggestedValue', + 'analysisSuggestedNoValue', + 'needsUserInput', + ], + description: + 'The outcome of the analysis for this particular field', + }, + field: { + type: 'string', + description: + 'e.g. "spec.owner"? The frontend needs to know how to "inject" the field into the\nentity again if the user wants to change it', + }, + }, + required: ['description', 'value', 'state', 'field'], + additionalProperties: false, + }, + AnalyzeLocationGenerateEntity: { + type: 'object', + properties: { + fields: { + type: 'array', + items: { + $ref: '#/components/schemas/AnalyzeLocationEntityField', + }, + }, + entity: { + $ref: '#/components/schemas/RecursivePartial_Entity_', + }, + }, + required: ['fields', 'entity'], + description: + "This is some form of representation of what the analyzer could deduce.\nWe should probably have a chat about how this can best be conveyed to\nthe frontend. It'll probably contain a (possibly incomplete) entity, plus\nenough info for the frontend to know what form data to show to the user\nfor overriding/completing the info.", + additionalProperties: false, + }, + AnalyzeLocationResponse: { + type: 'object', + properties: { + generateEntities: { + items: { + $ref: '#/components/schemas/AnalyzeLocationGenerateEntity', + }, + type: 'array', + }, + existingEntityFiles: { + items: { + $ref: '#/components/schemas/AnalyzeLocationExistingEntity', + }, + type: 'array', + }, + }, + required: ['generateEntities', 'existingEntityFiles'], + additionalProperties: false, + }, + LocationInput: { + type: 'object', + properties: { + type: { + type: 'string', + }, + target: { + type: 'string', + }, + presence: { + type: 'string', + enum: ['optional', 'required'], + }, + }, + required: ['type', 'target', 'presence'], + additionalProperties: false, + }, + SerializedError: { + allOf: [ + { + $ref: '#/components/schemas/JsonObject', + }, + { + type: 'object', + properties: { + code: { + type: 'string', + description: + 'A custom code (not necessarily the same as an HTTP response code); may not be present', + }, + stack: { + type: 'string', + description: 'A stringified stack trace; may not be present', + }, + message: { + type: 'string', + description: 'The message of the exception that was thrown', + }, + name: { + type: 'string', + description: 'The name of the exception that was thrown', + }, + }, + required: ['message', 'name'], + }, + ], + description: 'The serialized form of an Error.', + additionalProperties: false, + }, + EntitiesQueryResponse: { + type: 'object', + properties: { + items: { + type: 'array', + items: { + $ref: '#/components/schemas/Entity', + }, + description: 'The list of entities paginated by a specific filter.', + }, + totalItems: { + type: 'number', + }, + pageInfo: { + type: 'object', + properties: { + nextCursor: { + type: 'string', + description: 'Base64 encoded database query for the next page.', + }, + prevCursor: { + type: 'string', + description: + 'Base64 encoded database query for the previous page.', + }, + }, + }, + }, + additionalProperties: false, + }, + }, + securitySchemes: { + JWT: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + paths: { + '/refresh': { + post: { + operationId: 'RefreshEntity', + responses: { + '200': { + description: 'Refreshed', + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + authorizationToken: { + type: 'string', + }, + entityRef: { + type: 'string', + description: + 'The reference to a single entity that should be refreshed', + }, + }, + required: ['entityRef'], + description: + 'Options for requesting a refresh of entities in the catalog.', + additionalProperties: false, + }, + }, + }, + }, + }, + }, + '/entities': { + get: { + operationId: 'GetEntities', + responses: { + '200': { + description: '', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + $ref: '#/components/schemas/Entity', + }, + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + in: 'query', + name: 'filter', + required: false, + schema: { + type: 'string', + }, + }, + { + in: 'query', + name: 'fields', + required: false, + schema: { + type: 'string', + }, + }, + { + in: 'query', + name: 'offset', + required: false, + schema: { + type: 'string', + }, + }, + { + in: 'query', + name: 'limit', + required: false, + schema: { + type: 'string', + }, + }, + { + in: 'query', + name: 'after', + required: false, + schema: { + type: 'string', + }, + }, + ], + }, + }, + '/entities/by-uid/{uid}': { + get: { + operationId: 'GetEntityByUid', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Entity', + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + in: 'path', + name: 'uid', + required: true, + schema: { + type: 'string', + }, + }, + ], + }, + delete: { + operationId: 'DeleteEntityByUid', + responses: { + '204': { + description: 'Deleted successfully.', + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + in: 'path', + name: 'uid', + required: true, + schema: { + type: 'string', + }, + }, + ], + }, + }, + '/entities/by-name/{kind}/{namespace}/{name}': { + get: { + operationId: 'GetEntityByName', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Entity', + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + in: 'path', + name: 'kind', + required: true, + schema: { + type: 'string', + }, + }, + { + in: 'path', + name: 'namespace', + required: true, + schema: { + type: 'string', + }, + }, + { + in: 'path', + name: 'name', + required: true, + schema: { + type: 'string', + }, + }, + ], + }, + }, + '/entities/by-name/{kind}/{namespace}/{name}/ancestry': { + get: { + operationId: 'GetEntityAncestryByName', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/EntityAncestryResponse', + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + in: 'path', + name: 'kind', + required: true, + schema: { + type: 'string', + }, + }, + { + in: 'path', + name: 'namespace', + required: true, + schema: { + type: 'string', + }, + }, + { + in: 'path', + name: 'name', + required: true, + schema: { + type: 'string', + }, + }, + ], + }, + }, + '/entities/by-refs': { + post: { + operationId: 'GetEntitiesByRefs', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/EntitiesBatchResponse', + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + in: 'query', + name: 'fields', + required: false, + schema: { + type: 'string', + }, + }, + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + '/entities/by-query': { + get: { + operationId: 'GetEntitiesByQuery', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/EntitiesQueryResponse', + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + in: 'query', + name: 'fields', + required: false, + schema: { + type: 'string', + }, + }, + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + '/entity-facets': { + get: { + operationId: 'GetEntityFacets', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/EntityFacetsResponse', + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + in: 'query', + name: 'facet', + required: true, + schema: { + type: 'string', + }, + }, + { + in: 'query', + name: 'filter', + required: false, + schema: { + type: 'string', + }, + }, + ], + }, + }, + '/locations': { + post: { + operationId: 'CreateLocation', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + exists: { + type: 'boolean', + }, + entities: { + items: { + $ref: '#/components/schemas/Entity', + }, + type: 'array', + }, + location: { + $ref: '#/components/schemas/Location', + }, + }, + required: ['entities', 'location'], + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + in: 'query', + name: 'dryRun', + required: false, + schema: { + type: 'boolean', + }, + }, + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + presence: { + type: 'string', + enum: ['required', 'optional'], + }, + target: { + type: 'string', + }, + type: { + type: 'string', + }, + }, + required: ['presence', 'target', 'type'], + }, + }, + }, + }, + }, + get: { + operationId: 'GetLocations', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'object', + properties: { + data: { + $ref: '#/components/schemas/Location', + }, + }, + required: ['data'], + }, + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [], + }, + }, + '/locations/{id}': { + get: { + operationId: 'GetLocation', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Location', + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + in: 'path', + name: 'id', + required: true, + schema: { + type: 'string', + }, + }, + ], + }, + delete: { + operationId: 'DeleteLocation', + responses: { + '204': { + description: 'No content', + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + in: 'path', + name: 'id', + required: true, + schema: { + type: 'string', + }, + }, + ], + }, + }, + '/analyze-location': { + post: { + operationId: 'AnalyzeLocation', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/AnalyzeLocationResponse', + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + catalogFileName: { + type: 'string', + }, + location: { + $ref: '#/components/schemas/LocationInput', + }, + }, + required: ['catalogFileName', 'location'], + }, + }, + }, + }, + }, + }, + '/validate-entity': { + post: { + operationId: 'ValidateEntity', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + anyOf: [ + { + type: 'object', + properties: { + errors: { + $ref: '#/components/schemas/SerializedError', + }, + }, + required: ['errors'], + }, + { + type: 'object', + properties: { + errors: { + type: 'array', + items: { + $ref: '#/components/schemas/SerializedError', + }, + }, + }, + required: ['errors'], + }, + ], + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + location: { + type: 'string', + }, + entity: { + $ref: '#/components/schemas/Entity', + }, + }, + required: ['location', 'entity'], + }, + }, + }, + }, + }, + }, + }, +} as const; diff --git a/plugins/openapi-router-common/src/schema/petstore.ts b/plugins/openapi-router-common/src/schema/petstore.ts index 308e3925e0..a79a3efe4d 100644 --- a/plugins/openapi-router-common/src/schema/petstore.ts +++ b/plugins/openapi-router-common/src/schema/petstore.ts @@ -156,6 +156,7 @@ export default { type: 'string', }, }, + additionalProperties: false, }, Pets: { type: 'array', @@ -175,6 +176,7 @@ export default { type: 'string', }, }, + additionalProperties: false, }, }, }, diff --git a/plugins/openapi-router-common/src/types/common.ts b/plugins/openapi-router-common/src/types/common.ts index 834f1b49f5..ff404cb126 100644 --- a/plugins/openapi-router-common/src/types/common.ts +++ b/plugins/openapi-router-common/src/types/common.ts @@ -131,7 +131,7 @@ export type ObjectWithContentSchema< Object extends { content?: ContentObject }, > = Object['content'] extends ContentObject ? SchemaRef - : unknown; + : never; /** * From https://stackoverflow.com/questions/71393738/typescript-intersection-not-union-type-from-json-schema. @@ -164,3 +164,7 @@ export type ConvertAll = []> = T extends [ ] ? ConvertAll]> : R; + +type UnknownIfNever

= [P] extends [never] ? unknown : P; + +export type ToTypeSafe = UnknownIfNever>[number]>; diff --git a/plugins/openapi-router-common/src/types/express.ts b/plugins/openapi-router-common/src/types/express.ts index 055235af99..dc7bf61832 100644 --- a/plugins/openapi-router-common/src/types/express.ts +++ b/plugins/openapi-router-common/src/types/express.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Application } from 'express'; import core from 'express-serve-static-core'; import { DocPathTemplate, MethodAwareDocPath, RequiredDoc } from './common'; import { RequestBodyToJsonSchema } from './requests'; @@ -60,12 +59,8 @@ export interface DocRequestMatcher< | 'delete' | 'patch' | 'options' - | 'head' = any, + | 'head', > { - , Method>>( - path: Path, - ...handlers: DocRequestHandler[] - ): T; , Method>>( path: Path, ...handlers: Array> @@ -74,13 +69,4 @@ export interface DocRequestMatcher< path: Path, ...handlers: Array> ): T; - , Method>>( - path: PathParams, - ...handlers: Array> - ): T; - , Method>>( - path: PathParams, - ...handlers: Array> - ): T; - (path: PathParams, subApplication: Application): T; } diff --git a/plugins/openapi-router-common/src/types/requests.ts b/plugins/openapi-router-common/src/types/requests.ts index ae05a27537..6964f219a2 100644 --- a/plugins/openapi-router-common/src/types/requests.ts +++ b/plugins/openapi-router-common/src/types/requests.ts @@ -29,9 +29,7 @@ import type { DocPathMethod, DocPathTemplate, PathTemplate, - ConvertAll, - TuplifyUnion, - ValueOf, + ToTypeSafe, } from './common'; type RequestBody< @@ -54,12 +52,10 @@ export type RequestBodySchema< Method extends DocPathMethod, > = RequestBody, Method> extends RequestBodyObject ? ObjectWithContentSchema, Method>> - : unknown; + : never; export type RequestBodyToJsonSchema< Doc extends RequiredDoc, Path extends PathTemplate>, Method extends DocPathMethod, -> = ConvertAll< - TuplifyUnion>> ->[number]; +> = ToTypeSafe>; diff --git a/plugins/openapi-router-common/src/types/response.ts b/plugins/openapi-router-common/src/types/response.ts index 5c01a70673..f20d94c2a9 100644 --- a/plugins/openapi-router-common/src/types/response.ts +++ b/plugins/openapi-router-common/src/types/response.ts @@ -29,9 +29,8 @@ import type { DocPathMethod, DocPathTemplate, PathTemplate, - ConvertAll, + ToTypeSafe, ValueOf, - TuplifyUnion, } from './common'; type Response< @@ -85,13 +84,11 @@ export type ResponseSchemas< Doc, Responses, Method>[StatusCode] > - : unknown; + : never; }; export type ResponseBodyToJsonSchema< Doc extends RequiredDoc, Path extends PathTemplate>, Method extends DocPathMethod, -> = ConvertAll< - TuplifyUnion>> ->[number]; +> = ToTypeSafe>>;