@backstage/backend-openapi-utils 0.1.19-next.0 → 0.2.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ var errors = require('./errors.cjs.js');
4
+
5
+ class DisabledResponseBodyParser {
6
+ operation;
7
+ constructor(operation) {
8
+ this.operation = operation;
9
+ }
10
+ async parse(response) {
11
+ const body = await response.text();
12
+ if (body?.length) {
13
+ throw new errors.OperationError(
14
+ this.operation,
15
+ "Received a body but no schema was found"
16
+ );
17
+ }
18
+ return void 0;
19
+ }
20
+ }
21
+ class ResponseBodyParser {
22
+ operation;
23
+ ajv;
24
+ static fromOperation(operation, options) {
25
+ return operation.schema.responses && Object.keys(operation.schema.responses).length ? new ResponseBodyParser(operation, options) : new DisabledResponseBodyParser(operation);
26
+ }
27
+ constructor(operation, options) {
28
+ this.operation = operation;
29
+ this.ajv = options.ajv;
30
+ const responseSchemas = operation.schema.responses;
31
+ for (const [statusCode, schema] of Object.entries(responseSchemas)) {
32
+ const contentTypes = schema.content;
33
+ if (!contentTypes) {
34
+ continue;
35
+ }
36
+ const jsonContentType = Object.keys(contentTypes).find(
37
+ (contentType) => contentType.split(";").includes("application/json")
38
+ );
39
+ if (!jsonContentType) {
40
+ throw new errors.OperationError(
41
+ this.operation,
42
+ `No application/json content type found in response for status code ${statusCode}`
43
+ );
44
+ } else if ("$ref" in contentTypes[jsonContentType].schema) {
45
+ throw new errors.OperationError(
46
+ this.operation,
47
+ "Reference objects are not supported"
48
+ );
49
+ }
50
+ }
51
+ }
52
+ async parse(response) {
53
+ const body = await response.text();
54
+ const responseSchema = this.findResponseSchema(
55
+ this.operation.schema,
56
+ response
57
+ );
58
+ if (!responseSchema?.content && !body?.length) {
59
+ return void 0;
60
+ }
61
+ if (!responseSchema) {
62
+ throw new errors.OperationResponseError(
63
+ this.operation,
64
+ response,
65
+ `No schema found.`
66
+ );
67
+ }
68
+ const contentTypes = responseSchema.content;
69
+ if (!contentTypes && body?.length) {
70
+ throw new errors.OperationResponseError(
71
+ this.operation,
72
+ response,
73
+ "Received a body but no schema was found"
74
+ );
75
+ }
76
+ const jsonContentType = Object.keys(contentTypes ?? {}).find(
77
+ (contentType) => contentType.split(";").includes("application/json")
78
+ );
79
+ if (!jsonContentType) {
80
+ throw new errors.OperationResponseError(
81
+ this.operation,
82
+ response,
83
+ "No application/json content type found in response"
84
+ );
85
+ }
86
+ const schema = responseSchema.content[jsonContentType].schema;
87
+ if (!schema) {
88
+ throw new errors.OperationError(this.operation, "No schema found in response");
89
+ }
90
+ if ("$ref" in schema) {
91
+ throw new errors.OperationResponseError(
92
+ this.operation,
93
+ response,
94
+ "Reference objects are not supported"
95
+ );
96
+ }
97
+ if (!schema.required && !body?.length) {
98
+ throw new errors.OperationResponseError(
99
+ this.operation,
100
+ response,
101
+ "Response body is required but missing"
102
+ );
103
+ } else if (!schema.required && !body?.length) {
104
+ return void 0;
105
+ }
106
+ const validate = this.ajv.compile(schema);
107
+ const jsonBody = await response.json();
108
+ const valid = validate(jsonBody);
109
+ if (!valid) {
110
+ throw new errors.OperationParsingResponseError(
111
+ this.operation,
112
+ response,
113
+ "Response body",
114
+ validate.errors
115
+ );
116
+ }
117
+ return jsonBody;
118
+ }
119
+ findResponseSchema(operationSchema, { status }) {
120
+ return operationSchema.responses?.[status] ?? operationSchema.responses?.default;
121
+ }
122
+ }
123
+
124
+ exports.ResponseBodyParser = ResponseBodyParser;
125
+ //# sourceMappingURL=response-body-validation.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response-body-validation.cjs.js","sources":["../../src/schema/response-body-validation.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { JsonObject } from '@backstage/types';\nimport { Operation, ParserOptions, ResponseParser } from './types';\nimport {\n OperationError,\n OperationParsingResponseError,\n OperationResponseError,\n} from './errors';\nimport Ajv from 'ajv';\nimport { OperationObject, ResponseObject } from 'openapi3-ts';\n\nclass DisabledResponseBodyParser\n implements ResponseParser<JsonObject | undefined>\n{\n operation: Operation;\n constructor(operation: Operation) {\n this.operation = operation;\n }\n async parse(response: Response): Promise<JsonObject | undefined> {\n const body = await response.text();\n if (body?.length) {\n throw new OperationError(\n this.operation,\n 'Received a body but no schema was found',\n );\n }\n return undefined;\n }\n}\n\nexport class ResponseBodyParser\n implements ResponseParser<JsonObject | undefined>\n{\n operation: Operation;\n ajv: Ajv;\n\n static fromOperation(operation: Operation, options: ParserOptions) {\n return operation.schema.responses &&\n Object.keys(operation.schema.responses).length\n ? new ResponseBodyParser(operation, options)\n : new DisabledResponseBodyParser(operation);\n }\n\n constructor(operation: Operation, options: ParserOptions) {\n this.operation = operation;\n this.ajv = options.ajv;\n const responseSchemas = operation.schema.responses;\n for (const [statusCode, schema] of Object.entries(responseSchemas)) {\n const contentTypes = schema.content;\n if (!contentTypes) {\n // Skip responses without content, eg 204 No Content.\n continue;\n }\n const jsonContentType = Object.keys(contentTypes).find(contentType =>\n contentType.split(';').includes('application/json'),\n );\n if (!jsonContentType) {\n throw new OperationError(\n this.operation,\n `No application/json content type found in response for status code ${statusCode}`,\n );\n } else if ('$ref' in contentTypes[jsonContentType].schema) {\n throw new OperationError(\n this.operation,\n 'Reference objects are not supported',\n );\n }\n }\n }\n\n async parse(response: Response): Promise<JsonObject | undefined> {\n const body = await response.text();\n const responseSchema = this.findResponseSchema(\n this.operation.schema,\n response,\n );\n if (!responseSchema?.content && !body?.length) {\n // If there is no content in the response schema and no body in the response, then the response is valid.\n // eg 204 No Content\n return undefined;\n }\n if (!responseSchema) {\n throw new OperationResponseError(\n this.operation,\n response,\n `No schema found.`,\n );\n }\n\n const contentTypes = responseSchema.content;\n if (!contentTypes && body?.length) {\n throw new OperationResponseError(\n this.operation,\n response,\n 'Received a body but no schema was found',\n );\n }\n const jsonContentType = Object.keys(contentTypes ?? {}).find(contentType =>\n contentType.split(';').includes('application/json'),\n );\n if (!jsonContentType) {\n throw new OperationResponseError(\n this.operation,\n response,\n 'No application/json content type found in response',\n );\n }\n const schema = responseSchema.content![jsonContentType].schema;\n // This is a bit of type laziness. Ideally, this would be a type-narrowing function, but I wasn't able to get the types to work.\n if (!schema) {\n throw new OperationError(this.operation, 'No schema found in response');\n }\n if ('$ref' in schema) {\n throw new OperationResponseError(\n this.operation,\n response,\n 'Reference objects are not supported',\n );\n }\n\n if (!schema.required && !body?.length) {\n throw new OperationResponseError(\n this.operation,\n response,\n 'Response body is required but missing',\n );\n } else if (!schema.required && !body?.length) {\n // If there is no content in the response schema and no body in the response, then the response is valid\n return undefined;\n }\n\n const validate = this.ajv.compile(schema);\n const jsonBody = (await response.json()) as JsonObject;\n const valid = validate(jsonBody);\n if (!valid) {\n throw new OperationParsingResponseError(\n this.operation,\n response,\n 'Response body',\n validate.errors!,\n );\n }\n return jsonBody;\n }\n\n private findResponseSchema(\n operationSchema: OperationObject,\n { status }: Response,\n ): ResponseObject | undefined {\n return (\n operationSchema.responses?.[status] ?? operationSchema.responses?.default\n );\n }\n}\n"],"names":["OperationError","OperationResponseError","OperationParsingResponseError"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ function mockttpToFetchRequest(request) {
4
+ const headers = new Headers(request.rawHeaders);
5
+ return {
6
+ url: request.url,
7
+ method: request.method,
8
+ headers,
9
+ json: () => request.body.getJson(),
10
+ text: () => request.body.getText()
11
+ };
12
+ }
13
+ function mockttpToFetchResponse(response) {
14
+ const headers = new Headers(response.rawHeaders);
15
+ return {
16
+ status: response.statusCode,
17
+ headers,
18
+ json: () => response.body?.getJson(),
19
+ text: () => response.body?.getText()
20
+ };
21
+ }
22
+ function humanifyAjvError(error) {
23
+ switch (error.keyword) {
24
+ case "required":
25
+ return `The "${error.params.missingProperty}" property is required`;
26
+ case "type":
27
+ return `${error.instancePath ? `"${error.instancePath}"` : "Value"} should be of type ${error.params.type}`;
28
+ case "additionalProperties":
29
+ return `The "${error.params.additionalProperty}" property is not allowed`;
30
+ default:
31
+ return error.message;
32
+ }
33
+ }
34
+
35
+ exports.humanifyAjvError = humanifyAjvError;
36
+ exports.mockttpToFetchRequest = mockttpToFetchRequest;
37
+ exports.mockttpToFetchResponse = mockttpToFetchResponse;
38
+ //# sourceMappingURL=utils.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.cjs.js","sources":["../../src/schema/utils.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { CompletedRequest, CompletedResponse } from 'mockttp';\nimport { ErrorObject } from 'ajv';\n\nexport function mockttpToFetchRequest(request: CompletedRequest) {\n const headers = new Headers(request.rawHeaders);\n return {\n url: request.url,\n method: request.method,\n headers,\n json: () => request.body.getJson(),\n text: () => request.body.getText(),\n } as Request;\n}\nexport function mockttpToFetchResponse(response: CompletedResponse) {\n const headers = new Headers(response.rawHeaders);\n return {\n status: response.statusCode,\n headers,\n json: () => response.body?.getJson(),\n text: () => response.body?.getText(),\n } as Response;\n}\n\nexport function humanifyAjvError(error: ErrorObject) {\n switch (error.keyword) {\n case 'required':\n return `The \"${error.params.missingProperty}\" property is required`;\n case 'type':\n return `${\n error.instancePath ? `\"${error.instancePath}\"` : 'Value'\n } should be of type ${error.params.type}`;\n case 'additionalProperties':\n return `The \"${error.params.additionalProperty}\" property is not allowed`;\n default:\n return error.message;\n }\n}\n"],"names":[],"mappings":";;AAkBO,SAAS,sBAAsB,OAA2B,EAAA;AAC/D,EAAA,MAAM,OAAU,GAAA,IAAI,OAAQ,CAAA,OAAA,CAAQ,UAAU,CAAA,CAAA;AAC9C,EAAO,OAAA;AAAA,IACL,KAAK,OAAQ,CAAA,GAAA;AAAA,IACb,QAAQ,OAAQ,CAAA,MAAA;AAAA,IAChB,OAAA;AAAA,IACA,IAAM,EAAA,MAAM,OAAQ,CAAA,IAAA,CAAK,OAAQ,EAAA;AAAA,IACjC,IAAM,EAAA,MAAM,OAAQ,CAAA,IAAA,CAAK,OAAQ,EAAA;AAAA,GACnC,CAAA;AACF,CAAA;AACO,SAAS,uBAAuB,QAA6B,EAAA;AAClE,EAAA,MAAM,OAAU,GAAA,IAAI,OAAQ,CAAA,QAAA,CAAS,UAAU,CAAA,CAAA;AAC/C,EAAO,OAAA;AAAA,IACL,QAAQ,QAAS,CAAA,UAAA;AAAA,IACjB,OAAA;AAAA,IACA,IAAM,EAAA,MAAM,QAAS,CAAA,IAAA,EAAM,OAAQ,EAAA;AAAA,IACnC,IAAM,EAAA,MAAM,QAAS,CAAA,IAAA,EAAM,OAAQ,EAAA;AAAA,GACrC,CAAA;AACF,CAAA;AAEO,SAAS,iBAAiB,KAAoB,EAAA;AACnD,EAAA,QAAQ,MAAM,OAAS;AAAA,IACrB,KAAK,UAAA;AACH,MAAO,OAAA,CAAA,KAAA,EAAQ,KAAM,CAAA,MAAA,CAAO,eAAe,CAAA,sBAAA,CAAA,CAAA;AAAA,IAC7C,KAAK,MAAA;AACH,MAAO,OAAA,CAAA,EACL,KAAM,CAAA,YAAA,GAAe,CAAI,CAAA,EAAA,KAAA,CAAM,YAAY,CAAA,CAAA,CAAA,GAAM,OACnD,CAAA,mBAAA,EAAsB,KAAM,CAAA,MAAA,CAAO,IAAI,CAAA,CAAA,CAAA;AAAA,IACzC,KAAK,sBAAA;AACH,MAAO,OAAA,CAAA,KAAA,EAAQ,KAAM,CAAA,MAAA,CAAO,kBAAkB,CAAA,yBAAA,CAAA,CAAA;AAAA,IAChD;AACE,MAAA,OAAO,KAAM,CAAA,OAAA,CAAA;AAAA,GACjB;AACF;;;;;;"}
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ var Ajv = require('ajv');
4
+ var Parser = require('@apidevtools/swagger-parser');
5
+ var parameterValidation = require('./parameter-validation.cjs.js');
6
+ var errors = require('./errors.cjs.js');
7
+ var requestBodyValidation = require('./request-body-validation.cjs.js');
8
+ var utils = require('./utils.cjs.js');
9
+ var responseBodyValidation = require('./response-body-validation.cjs.js');
10
+
11
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
12
+
13
+ var Ajv__default = /*#__PURE__*/_interopDefaultCompat(Ajv);
14
+ var Parser__default = /*#__PURE__*/_interopDefaultCompat(Parser);
15
+
16
+ const ajv = new Ajv__default.default({ allErrors: true });
17
+ class RequestBodyValidator {
18
+ schema;
19
+ constructor(schema) {
20
+ this.schema = schema;
21
+ }
22
+ async validate({ pair, operation }) {
23
+ const { request, response } = pair;
24
+ if (response.statusCode === 400) {
25
+ return;
26
+ }
27
+ const parser = requestBodyValidation.RequestBodyParser.fromOperation(operation, { ajv });
28
+ const fetchRequest = utils.mockttpToFetchRequest(request);
29
+ await parser.parse(fetchRequest);
30
+ }
31
+ }
32
+ class ResponseBodyValidator {
33
+ schema;
34
+ constructor(schema) {
35
+ this.schema = schema;
36
+ }
37
+ async validate({ pair, operation }) {
38
+ const { response } = pair;
39
+ const parser = responseBodyValidation.ResponseBodyParser.fromOperation(operation, { ajv });
40
+ const fetchResponse = utils.mockttpToFetchResponse(response);
41
+ await parser.parse(fetchResponse);
42
+ }
43
+ }
44
+ function findOperationByRequest(openApiSchema, request) {
45
+ const { url } = request;
46
+ const { pathname } = new URL(url);
47
+ const parts = pathname.split("/");
48
+ for (const [path, schema] of Object.entries(openApiSchema.paths)) {
49
+ const pathParts = path.split("/");
50
+ if (parts.length !== pathParts.length) {
51
+ continue;
52
+ }
53
+ let found = true;
54
+ for (let i = 0; i < parts.length; i++) {
55
+ if (pathParts[i] === parts[i]) {
56
+ continue;
57
+ }
58
+ if (pathParts[i].startsWith("{") && pathParts[i].endsWith("}")) {
59
+ continue;
60
+ }
61
+ found = false;
62
+ break;
63
+ }
64
+ if (!found) {
65
+ continue;
66
+ }
67
+ let matchingOperationType = void 0;
68
+ for (const [operationType, operation] of Object.entries(schema)) {
69
+ if (operationType === request.method.toLowerCase()) {
70
+ matchingOperationType = operation;
71
+ break;
72
+ }
73
+ }
74
+ if (!matchingOperationType) {
75
+ continue;
76
+ }
77
+ return [path, matchingOperationType];
78
+ }
79
+ return void 0;
80
+ }
81
+ class OpenApiProxyValidator {
82
+ schema;
83
+ validators;
84
+ async initialize(url) {
85
+ this.schema = await Parser__default.default.dereference(url);
86
+ this.validators = [
87
+ new parameterValidation.ParameterValidator(this.schema),
88
+ new RequestBodyValidator(this.schema),
89
+ new ResponseBodyValidator(this.schema)
90
+ ];
91
+ }
92
+ async validate(request, response) {
93
+ const operationPathTuple = findOperationByRequest(this.schema, request);
94
+ if (!operationPathTuple) {
95
+ throw new errors.OperationError(
96
+ { path: request.path, method: request.method },
97
+ `No operation schema found for ${request.url}`
98
+ );
99
+ }
100
+ const [path, operationSchema] = operationPathTuple;
101
+ const operation = { path, method: request.method, schema: operationSchema };
102
+ const validators = this.validators;
103
+ await Promise.all(
104
+ validators.map(
105
+ (validator) => validator.validate({
106
+ pair: { request, response },
107
+ operation
108
+ })
109
+ )
110
+ );
111
+ }
112
+ }
113
+
114
+ exports.OpenApiProxyValidator = OpenApiProxyValidator;
115
+ exports.findOperationByRequest = findOperationByRequest;
116
+ //# sourceMappingURL=validation.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.cjs.js","sources":["../../src/schema/validation.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { CompletedRequest, CompletedResponse } from 'mockttp';\nimport { OpenAPIObject, OperationObject } from 'openapi3-ts';\nimport Ajv from 'ajv';\nimport Parser from '@apidevtools/swagger-parser';\nimport { Operation, Validator, ValidatorParams } from './types';\nimport { ParameterValidator } from './parameter-validation';\nimport { OperationError } from './errors';\nimport { RequestBodyParser } from './request-body-validation';\nimport { mockttpToFetchRequest, mockttpToFetchResponse } from './utils';\nimport { ResponseBodyParser } from './response-body-validation';\n\nconst ajv = new Ajv({ allErrors: true });\n\nclass RequestBodyValidator implements Validator {\n schema: OpenAPIObject;\n constructor(schema: OpenAPIObject) {\n this.schema = schema;\n }\n\n async validate({ pair, operation }: ValidatorParams) {\n const { request, response } = pair;\n if (response.statusCode === 400) {\n // If the response is a 400, then the request is invalid and we shouldn't validate the parameters\n return;\n }\n\n // NOTE: There may be a worthwhile optimization here to cache these results to avoid re-parsing the schema for every request. As is, I don't think this is a big deal.\n const parser = RequestBodyParser.fromOperation(operation, { ajv });\n const fetchRequest = mockttpToFetchRequest(request);\n await parser.parse(fetchRequest);\n }\n}\n\nclass ResponseBodyValidator implements Validator {\n schema: OpenAPIObject;\n constructor(schema: OpenAPIObject) {\n this.schema = schema;\n }\n\n async validate({ pair, operation }: ValidatorParams) {\n const { response } = pair;\n // NOTE: There may be a worthwhile optimization here to cache these results to avoid re-parsing the schema for every request. As is, I don't think this is a big deal.\n const parser = ResponseBodyParser.fromOperation(operation, { ajv });\n const fetchResponse = mockttpToFetchResponse(response);\n await parser.parse(fetchResponse);\n }\n}\n\n/**\n * Find an operation in an OpenAPI schema that matches a request. This is done by comparing the request URL to the paths in the schema.\n * @param openApiSchema - The OpenAPI schema to search for the operation in.\n * @param request - The request to find the operation for.\n * @returns A tuple of the path and the operation object that matches the request.\n */\nexport function findOperationByRequest(\n openApiSchema: OpenAPIObject,\n request: CompletedRequest,\n): [string, OperationObject] | undefined {\n const { url } = request;\n const { pathname } = new URL(url);\n\n const parts = pathname.split('/');\n for (const [path, schema] of Object.entries(openApiSchema.paths)) {\n const pathParts = path.split('/');\n if (parts.length !== pathParts.length) {\n continue;\n }\n let found = true;\n for (let i = 0; i < parts.length; i++) {\n if (pathParts[i] === parts[i]) {\n continue;\n }\n // If the path part is a parameter, we can count it as a match. eg /api/{id} will match /api/1\n if (pathParts[i].startsWith('{') && pathParts[i].endsWith('}')) {\n continue;\n }\n found = false;\n break;\n }\n if (!found) {\n continue;\n }\n let matchingOperationType: OperationObject | undefined = undefined;\n for (const [operationType, operation] of Object.entries(schema)) {\n if (operationType === request.method.toLowerCase()) {\n matchingOperationType = operation as OperationObject;\n break;\n }\n }\n if (!matchingOperationType) {\n continue;\n }\n return [path, matchingOperationType];\n }\n\n return undefined;\n}\n\nexport class OpenApiProxyValidator {\n schema: OpenAPIObject | undefined;\n validators: Validator[] | undefined;\n\n async initialize(url: string) {\n this.schema = (await Parser.dereference(url)) as unknown as OpenAPIObject;\n this.validators = [\n new ParameterValidator(this.schema),\n new RequestBodyValidator(this.schema),\n new ResponseBodyValidator(this.schema),\n ];\n }\n\n async validate(request: CompletedRequest, response: CompletedResponse) {\n const operationPathTuple = findOperationByRequest(this.schema!, request);\n if (!operationPathTuple) {\n throw new OperationError(\n { path: request.path, method: request.method } as Operation,\n `No operation schema found for ${request.url}`,\n );\n }\n\n const [path, operationSchema] = operationPathTuple;\n const operation = { path, method: request.method, schema: operationSchema };\n\n const validators = this.validators!;\n await Promise.all(\n validators.map(validator =>\n validator.validate({\n pair: { request, response },\n operation,\n }),\n ),\n );\n }\n}\n"],"names":["Ajv","RequestBodyParser","mockttpToFetchRequest","ResponseBodyParser","mockttpToFetchResponse","Parser","ParameterValidator","OperationError"],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ var PromiseRouter = require('express-promise-router');
4
+ var express = require('express');
5
+ var errors = require('@backstage/errors');
6
+ var expressOpenapiValidator = require('express-openapi-validator');
7
+ var constants = require('./constants.cjs.js');
8
+ var openapiMerge = require('openapi-merge');
9
+
10
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
11
+
12
+ var PromiseRouter__default = /*#__PURE__*/_interopDefaultCompat(PromiseRouter);
13
+
14
+ const baseUrlSymbol = Symbol();
15
+ const originalUrlSymbol = Symbol();
16
+ function validatorErrorTransformer() {
17
+ return (error, _, _2, next) => {
18
+ next(new errors.InputError(error.message));
19
+ };
20
+ }
21
+ function getDefaultRouterMiddleware() {
22
+ return [express.json()];
23
+ }
24
+ function getOpenApiSpecRoute(baseUrl) {
25
+ return `${baseUrl}${constants.OPENAPI_SPEC_ROUTE}`;
26
+ }
27
+ function createValidatedOpenApiRouter(spec, options) {
28
+ const router = PromiseRouter__default.default();
29
+ router.use(options?.middleware || getDefaultRouterMiddleware());
30
+ router.use((req, _, next) => {
31
+ const customRequest = req;
32
+ customRequest[baseUrlSymbol] = customRequest.baseUrl;
33
+ customRequest.baseUrl = "";
34
+ customRequest[originalUrlSymbol] = customRequest.originalUrl;
35
+ customRequest.originalUrl = customRequest.url;
36
+ next();
37
+ });
38
+ router.use(
39
+ expressOpenapiValidator.middleware({
40
+ validateRequests: {
41
+ coerceTypes: false,
42
+ allowUnknownQueryParameters: false
43
+ },
44
+ ignoreUndocumented: true,
45
+ validateResponses: false,
46
+ ...options?.validatorOptions,
47
+ apiSpec: spec
48
+ })
49
+ );
50
+ router.use((req, _, next) => {
51
+ const customRequest = req;
52
+ customRequest.baseUrl = customRequest[baseUrlSymbol];
53
+ customRequest.originalUrl = customRequest[originalUrlSymbol];
54
+ delete customRequest[baseUrlSymbol];
55
+ delete customRequest[originalUrlSymbol];
56
+ next();
57
+ });
58
+ router.use(validatorErrorTransformer());
59
+ router.get(constants.OPENAPI_SPEC_ROUTE, async (req, res) => {
60
+ const mergeOutput = openapiMerge.merge([
61
+ {
62
+ oas: spec,
63
+ pathModification: {
64
+ /**
65
+ * Get the route that this OpenAPI spec is hosted on. The other
66
+ * approach of using the discovery API increases the router constructor
67
+ * significantly and since we're just looking for path and not full URL,
68
+ * this works.
69
+ *
70
+ * If we wanted to add a list of servers, there may be a case for adding
71
+ * discovery API to get an exhaustive list of upstream servers, but that's
72
+ * also not currently supported.
73
+ */
74
+ prepend: req.originalUrl.replace(constants.OPENAPI_SPEC_ROUTE, "")
75
+ }
76
+ }
77
+ ]);
78
+ if (openapiMerge.isErrorResult(mergeOutput)) {
79
+ throw new errors.InputError("Invalid spec defined");
80
+ }
81
+ res.json(mergeOutput.output);
82
+ });
83
+ return router;
84
+ }
85
+
86
+ exports.createValidatedOpenApiRouter = createValidatedOpenApiRouter;
87
+ exports.getDefaultRouterMiddleware = getDefaultRouterMiddleware;
88
+ exports.getOpenApiSpecRoute = getOpenApiSpecRoute;
89
+ //# sourceMappingURL=stub.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stub.cjs.js","sources":["../src/stub.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport PromiseRouter from 'express-promise-router';\nimport { ApiRouter } from './router';\nimport { RequiredDoc } from './types';\nimport {\n ErrorRequestHandler,\n RequestHandler,\n NextFunction,\n Request,\n Response,\n json,\n} from 'express';\nimport { InputError } from '@backstage/errors';\nimport { middleware as OpenApiValidator } from 'express-openapi-validator';\nimport { OPENAPI_SPEC_ROUTE } from './constants';\nimport { isErrorResult, merge } from 'openapi-merge';\n\ntype PropertyOverrideRequest = Request & {\n [key: symbol]: string;\n};\n\nconst baseUrlSymbol = Symbol();\nconst originalUrlSymbol = Symbol();\n\nfunction validatorErrorTransformer(): ErrorRequestHandler {\n return (error: Error, _: Request, _2: Response, next: NextFunction) => {\n next(new InputError(error.message));\n };\n}\n\nexport function getDefaultRouterMiddleware() {\n return [json()];\n}\n\n/**\n * Given a base url for a plugin, find the given OpenAPI spec for that plugin.\n * @param baseUrl - Plugin base url.\n * @returns OpenAPI spec route for the base url.\n * @public\n */\nexport function getOpenApiSpecRoute(baseUrl: string) {\n return `${baseUrl}${OPENAPI_SPEC_ROUTE}`;\n}\n\n/**\n * Create a new OpenAPI router with some default middleware.\n * @param spec - Your OpenAPI spec imported as a JSON object.\n * @param validatorOptions - `openapi-express-validator` options to override the defaults.\n * @returns A new express router with validation middleware.\n * @public\n */\nexport function createValidatedOpenApiRouter<T extends RequiredDoc>(\n spec: T,\n options?: {\n validatorOptions?: Partial<Parameters<typeof OpenApiValidator>['0']>;\n middleware?: RequestHandler[];\n },\n) {\n const router = PromiseRouter();\n router.use(options?.middleware || getDefaultRouterMiddleware());\n\n /**\n * Middleware to setup the routing for OpenApiValidator. OpenApiValidator expects `req.originalUrl`\n * and `req.baseUrl` to be the full path. We adjust them here to basically be nothing and then\n * revive the old values in the last function in this method. We could instead update `req.path`\n * but that might affect the routing and I'd rather not.\n *\n * TODO: I opened https://github.com/cdimascio/express-openapi-validator/issues/843\n * to track this on the middleware side, but there was a similar ticket, https://github.com/cdimascio/express-openapi-validator/issues/113\n * that has had minimal activity. If that changes, update this to use a new option on their side.\n */\n router.use((req: Request, _, next) => {\n /**\n * Express typings are weird. They don't recognize PropertyOverrideRequest as a valid\n * Request child and try to overload as PathParams. Just cast it here, since we know\n * what we're doing.\n */\n const customRequest = req as PropertyOverrideRequest;\n customRequest[baseUrlSymbol] = customRequest.baseUrl;\n customRequest.baseUrl = '';\n customRequest[originalUrlSymbol] = customRequest.originalUrl;\n customRequest.originalUrl = customRequest.url;\n next();\n });\n\n // TODO: Handle errors by converting from OpenApiValidator errors to known @backstage/errors errors.\n router.use(\n OpenApiValidator({\n validateRequests: {\n coerceTypes: false,\n allowUnknownQueryParameters: false,\n },\n ignoreUndocumented: true,\n validateResponses: false,\n ...options?.validatorOptions,\n apiSpec: spec as any,\n }),\n );\n\n /**\n * Revert `req.baseUrl` and `req.originalUrl` changes. This ensures that any further usage\n * of these variables will be unchanged.\n */\n router.use((req: Request, _, next) => {\n const customRequest = req as PropertyOverrideRequest;\n customRequest.baseUrl = customRequest[baseUrlSymbol];\n customRequest.originalUrl = customRequest[originalUrlSymbol];\n delete customRequest[baseUrlSymbol];\n delete customRequest[originalUrlSymbol];\n next();\n });\n\n // Any errors from the middleware get through here.\n router.use(validatorErrorTransformer());\n\n router.get(OPENAPI_SPEC_ROUTE, async (req, res) => {\n const mergeOutput = merge([\n {\n oas: spec as any,\n pathModification: {\n /**\n * Get the route that this OpenAPI spec is hosted on. The other\n * approach of using the discovery API increases the router constructor\n * significantly and since we're just looking for path and not full URL,\n * this works.\n *\n * If we wanted to add a list of servers, there may be a case for adding\n * discovery API to get an exhaustive list of upstream servers, but that's\n * also not currently supported.\n */\n prepend: req.originalUrl.replace(OPENAPI_SPEC_ROUTE, ''),\n },\n },\n ]);\n if (isErrorResult(mergeOutput)) {\n throw new InputError('Invalid spec defined');\n }\n res.json(mergeOutput.output);\n });\n\n return router as ApiRouter<typeof spec>;\n}\n"],"names":["InputError","json","OPENAPI_SPEC_ROUTE","PromiseRouter","OpenApiValidator","merge","isErrorResult"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ var setup = require('./proxy/setup.cjs.js');
4
+
5
+ const proxiesToCleanup = [];
6
+ async function wrapServer(app) {
7
+ const proxy = new setup.Proxy();
8
+ proxiesToCleanup.push(proxy);
9
+ await proxy.setup();
10
+ const server = app.listen(proxy.forwardTo.port);
11
+ await proxy.initialize(`http://localhost:${proxy.forwardTo.port}`, server);
12
+ return { ...server, address: () => new URL(proxy.url) };
13
+ }
14
+ let registered = false;
15
+ function registerHooks() {
16
+ if (typeof afterAll !== "function" || typeof beforeAll !== "function") {
17
+ return;
18
+ }
19
+ if (registered) {
20
+ return;
21
+ }
22
+ registered = true;
23
+ afterAll(() => {
24
+ for (const proxy of proxiesToCleanup) {
25
+ proxy.stop();
26
+ }
27
+ });
28
+ }
29
+ registerHooks();
30
+ const wrapInOpenApiTestServer = (app) => {
31
+ if (process.env.OPTIC_PROXY) {
32
+ const server = app.listen(+process.env.PORT);
33
+ return {
34
+ ...server,
35
+ address: () => new URL(process.env.OPTIC_PROXY)
36
+ };
37
+ }
38
+ return app;
39
+ };
40
+
41
+ exports.wrapInOpenApiTestServer = wrapInOpenApiTestServer;
42
+ exports.wrapServer = wrapServer;
43
+ //# sourceMappingURL=testUtils.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testUtils.cjs.js","sources":["../src/testUtils.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { Express } from 'express';\nimport { Server } from 'http';\nimport { Proxy } from './proxy/setup';\n\nconst proxiesToCleanup: Proxy[] = [];\n\n/**\n * !!! THIS CURRENTLY ONLY SUPPORTS SUPERTEST !!!\n * Setup a server with a custom OpenAPI proxy. This proxy will capture all requests and responses and make sure they\n * conform to the spec.\n * @param app - express server, needed to ensure we have the correct ports for the proxy.\n * @returns - a configured HTTP server that should be used with supertest.\n * @public\n */\nexport async function wrapServer(app: Express): Promise<Server> {\n const proxy = new Proxy();\n proxiesToCleanup.push(proxy);\n await proxy.setup();\n\n const server = app.listen(proxy.forwardTo.port);\n await proxy.initialize(`http://localhost:${proxy.forwardTo.port}`, server);\n\n return { ...server, address: () => new URL(proxy.url) } as any;\n}\n\nlet registered = false;\nfunction registerHooks() {\n if (typeof afterAll !== 'function' || typeof beforeAll !== 'function') {\n return;\n }\n if (registered) {\n return;\n }\n registered = true;\n\n afterAll(() => {\n for (const proxy of proxiesToCleanup) {\n proxy.stop();\n }\n });\n}\n\nregisterHooks();\n\n/**\n * !!! THIS CURRENTLY ONLY SUPPORTS SUPERTEST !!!\n * Running against supertest, we need some way to hit the optic proxy. This ensures that\n * that happens at runtime when in the context of a `yarn optic capture` command.\n * @param app - Express router that would be passed to supertest's `request`.\n * @returns A wrapper around the express router (or the router untouched) that still works with supertest.\n * @public\n */\nexport const wrapInOpenApiTestServer = (app: Express): Server | Express => {\n if (process.env.OPTIC_PROXY) {\n const server = app.listen(+process.env.PORT!);\n return {\n ...server,\n address: () => new URL(process.env.OPTIC_PROXY!),\n } as any;\n }\n return app;\n};\n"],"names":["Proxy"],"mappings":";;;;AAmBA,MAAM,mBAA4B,EAAC,CAAA;AAUnC,eAAsB,WAAW,GAA+B,EAAA;AAC9D,EAAM,MAAA,KAAA,GAAQ,IAAIA,WAAM,EAAA,CAAA;AACxB,EAAA,gBAAA,CAAiB,KAAK,KAAK,CAAA,CAAA;AAC3B,EAAA,MAAM,MAAM,KAAM,EAAA,CAAA;AAElB,EAAA,MAAM,MAAS,GAAA,GAAA,CAAI,MAAO,CAAA,KAAA,CAAM,UAAU,IAAI,CAAA,CAAA;AAC9C,EAAA,MAAM,MAAM,UAAW,CAAA,CAAA,iBAAA,EAAoB,MAAM,SAAU,CAAA,IAAI,IAAI,MAAM,CAAA,CAAA;AAEzE,EAAO,OAAA,EAAE,GAAG,MAAQ,EAAA,OAAA,EAAS,MAAM,IAAI,GAAA,CAAI,KAAM,CAAA,GAAG,CAAE,EAAA,CAAA;AACxD,CAAA;AAEA,IAAI,UAAa,GAAA,KAAA,CAAA;AACjB,SAAS,aAAgB,GAAA;AACvB,EAAA,IAAI,OAAO,QAAA,KAAa,UAAc,IAAA,OAAO,cAAc,UAAY,EAAA;AACrE,IAAA,OAAA;AAAA,GACF;AACA,EAAA,IAAI,UAAY,EAAA;AACd,IAAA,OAAA;AAAA,GACF;AACA,EAAa,UAAA,GAAA,IAAA,CAAA;AAEb,EAAA,QAAA,CAAS,MAAM;AACb,IAAA,KAAA,MAAW,SAAS,gBAAkB,EAAA;AACpC,MAAA,KAAA,CAAM,IAAK,EAAA,CAAA;AAAA,KACb;AAAA,GACD,CAAA,CAAA;AACH,CAAA;AAEA,aAAc,EAAA,CAAA;AAUD,MAAA,uBAAA,GAA0B,CAAC,GAAmC,KAAA;AACzE,EAAI,IAAA,OAAA,CAAQ,IAAI,WAAa,EAAA;AAC3B,IAAA,MAAM,SAAS,GAAI,CAAA,MAAA,CAAO,CAAC,OAAA,CAAQ,IAAI,IAAK,CAAA,CAAA;AAC5C,IAAO,OAAA;AAAA,MACL,GAAG,MAAA;AAAA,MACH,SAAS,MAAM,IAAI,GAAI,CAAA,OAAA,CAAQ,IAAI,WAAY,CAAA;AAAA,KACjD,CAAA;AAAA,GACF;AACA,EAAO,OAAA,GAAA,CAAA;AACT;;;;;"}
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/backend-openapi-utils",
3
- "version": "0.1.19-next.0",
3
+ "version": "0.2.0-next.1",
4
4
  "description": "OpenAPI typescript support.",
5
5
  "backstage": {
6
6
  "role": "node-library"
@@ -33,20 +33,27 @@
33
33
  "test": "backstage-cli package test"
34
34
  },
35
35
  "dependencies": {
36
- "@backstage/backend-plugin-api": "^1.0.1-next.0",
37
- "@backstage/errors": "^1.2.4",
36
+ "@apidevtools/swagger-parser": "^10.1.0",
37
+ "@backstage/backend-plugin-api": "1.0.1-next.1",
38
+ "@backstage/errors": "1.2.4",
39
+ "@backstage/types": "1.1.1",
38
40
  "@types/express": "^4.17.6",
39
41
  "@types/express-serve-static-core": "^4.17.5",
42
+ "ajv": "^8.16.0",
40
43
  "express": "^4.17.1",
41
44
  "express-openapi-validator": "^5.0.4",
42
45
  "express-promise-router": "^4.1.0",
46
+ "get-port": "^5.1.1",
43
47
  "json-schema-to-ts": "^3.0.0",
44
48
  "lodash": "^4.17.21",
49
+ "mockttp": "^3.13.0",
50
+ "msw": "^1.0.0",
45
51
  "openapi-merge": "^1.3.2",
46
52
  "openapi3-ts": "^3.1.2"
47
53
  },
48
54
  "devDependencies": {
49
- "@backstage/cli": "^0.28.0-next.0",
55
+ "@backstage/cli": "0.28.0-next.2",
56
+ "@backstage/test-utils": "1.6.1-next.2",
50
57
  "supertest": "^7.0.0"
51
58
  },
52
59
  "module": "dist/index.esm.js"