@backstage/backend-openapi-utils 0.0.2-next.1 → 0.0.3-next.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @backstage/backend-openapi-utils
2
2
 
3
+ ## 0.0.3-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - ebeb77586975: Add a new `createRouter` method for generating an `express` router that validates against your spec. Also fixes a bug with the query parameters type resolution.
8
+ - Updated dependencies
9
+ - @backstage/errors@1.2.1
10
+
11
+ ## 0.0.2
12
+
13
+ ### Patch Changes
14
+
15
+ - fe16bd39e83: Use permalinks for links including a line number reference
16
+ - 27956d78671: Adjusted README accordingly after the generated output now has a `.generated.ts` extension
17
+ - 021cfbb5152: Corrected resolution of parameter nested schema to use central schemas.
18
+ - 799c33047ed: Updated README to reflect changes in `@backstage/repo-tools`.
19
+
3
20
  ## 0.0.2-next.1
4
21
 
5
22
  ### Patch Changes
package/README.md CHANGED
@@ -8,21 +8,61 @@ This package is meant to provide a typed Express router for an OpenAPI spec. Bas
8
8
 
9
9
  ### Configuration
10
10
 
11
- 1. Run `yarn --cwd <package-dir> backstage-cli package schema:openapi:generate` to translate your `src/schema/openapi.yaml` to a new Typescript file in `src/schema/openapi.generated.ts`. The command will try to execute both a lint and prettier step on the generated file, where applicable.
11
+ 1. Run `yarn --cwd <package-dir> backstage-cli package schema openapi generate` to translate your `src/schema/openapi.yaml` to a new Typescript file in `src/schema/openapi.generated.ts`. The command will try to execute both a lint and prettier step on the generated file, where applicable.
12
12
 
13
13
  2. In your plugin's `src/service/createRouter.ts`,
14
14
 
15
15
  ```ts
16
- import { ApiRouter } from `@backstage/backend-openapi-utils`;
17
- import spec from '../schema/openapi.generated';
16
+ import { createOpenApiRouter } from '../schema/openapi.generated';
18
17
  // ...
19
18
  export function createRouter() {
20
- const router = Router() as ApiRouter<typeof spec>;
21
- // ...
22
- return router;
19
+ const router = createOpenApiRouter();
20
+ // add routes to router, it's just an express router.
21
+ return router;
23
22
  }
24
23
  ```
25
24
 
25
+ 3. Add `@backstage/backend-openapi-utils` to your `package.json`'s `dependencies`.
26
+
27
+ Why do I need to add this to `dependencies`? If you check the `src/schema/openapi.generated.ts` file, we're creating a router stub for you with the `@backstage/backend-openapi-utils` package.
28
+
29
+ ### Customization
30
+
31
+ If the out of the box `router` doesn't work, you can do the following,
32
+
33
+ ```ts
34
+ import { createOpenApiRouter } from '../schema/openapi.generated';
35
+ // ...
36
+ export function createRouter() {
37
+ // See https://github.com/cdimascio/express-openapi-validator/wiki/Documentation for available options.
38
+ const router = createOpenApiRouter(validatorOptions);
39
+ // add routes to router, it's just an express router.
40
+ return router;
41
+ }
42
+ ```
43
+
44
+ If you need even more control -- say for example you wanted to update the spec at runtime -- you can do the following,
45
+
46
+ ```ts
47
+ import { spec } from '../schema/openapi.generated';
48
+ import { createValidatedOpenApiRouter } from '@backstage/backend-openapi-utils';
49
+ // ...
50
+ export function createRouter() {
51
+ // Update the spec here.
52
+ const newSpec = { ...spec, myproperty123: 123 };
53
+
54
+ // See https://github.com/cdimascio/express-openapi-validator/wiki/Documentation for available options.
55
+ const router = createValidatedOpenApiRouter<typeof newSpec>(
56
+ newSpec,
57
+ validatorOptions,
58
+ );
59
+ // add routes to router, it's just an express router.
60
+ return router;
61
+ }
62
+ ```
63
+
64
+ ## INTERNAL
65
+
26
66
  ### Limitations
27
67
 
28
68
  1. `as const` makes all fields `readonly`
@@ -40,6 +80,10 @@ Router() as ApiRouter<DeepWriteable<typeof spec>>
40
80
 
41
81
  ## Future Work
42
82
 
43
- ### Runtime validation
83
+ ### Response Validation
84
+
85
+ This is a murky ground and something that will take a while to gain adoption. For now, keep responses in the spec and at the type level, but will need to work to drive adoption of response validation.
86
+
87
+ ### Common Error Format
44
88
 
45
- Using a package like [`express-openapi-validator`](https://www.npmjs.com/package/express-openapi-validator), would allow us to remove [validation of request bodies with `AJV`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/catalog-backend/src/service/util.ts#L58). However, `AJV` currently doesn't have support for OpenAPI 3.1 and `express-openapi-validator` enforces full URL matching for paths, meaning it cannot be mounted at the router level.
89
+ With the new `createRouter` method, we can start to control error response formats for input and coercion errors.
package/dist/index.cjs.js CHANGED
@@ -2,9 +2,64 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var PromiseRouter = require('express-promise-router');
6
+ var express = require('express');
7
+ var errors = require('@backstage/errors');
8
+ var expressOpenapiValidator = require('express-openapi-validator');
9
+
10
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
11
+
12
+ var PromiseRouter__default = /*#__PURE__*/_interopDefaultLegacy(PromiseRouter);
13
+
5
14
  var index = /*#__PURE__*/Object.freeze({
6
- __proto__: null
15
+ __proto__: null
7
16
  });
8
17
 
18
+ const baseUrlSymbol = Symbol();
19
+ const originalUrlSymbol = Symbol();
20
+ function validatorErrorTransformer() {
21
+ return (error, _, _2, next) => {
22
+ next(new errors.InputError(error.message));
23
+ };
24
+ }
25
+ function getDefaultRouterMiddleware() {
26
+ return [express.json()];
27
+ }
28
+ function createValidatedOpenApiRouter(spec, options) {
29
+ const router = PromiseRouter__default["default"]();
30
+ router.use((options == null ? void 0 : options.middleware) || getDefaultRouterMiddleware());
31
+ router.use((req, _, next) => {
32
+ const customRequest = req;
33
+ customRequest[baseUrlSymbol] = customRequest.baseUrl;
34
+ customRequest.baseUrl = "";
35
+ customRequest[originalUrlSymbol] = customRequest.originalUrl;
36
+ customRequest.originalUrl = customRequest.url;
37
+ next();
38
+ });
39
+ router.use(
40
+ expressOpenapiValidator.middleware({
41
+ validateRequests: {
42
+ coerceTypes: false,
43
+ allowUnknownQueryParameters: false
44
+ },
45
+ ignoreUndocumented: true,
46
+ validateResponses: false,
47
+ ...options == null ? void 0 : options.validatorOptions,
48
+ apiSpec: spec
49
+ })
50
+ );
51
+ router.use((req, _, next) => {
52
+ const customRequest = req;
53
+ customRequest.baseUrl = customRequest[baseUrlSymbol];
54
+ customRequest.originalUrl = customRequest[originalUrlSymbol];
55
+ delete customRequest[baseUrlSymbol];
56
+ delete customRequest[originalUrlSymbol];
57
+ next();
58
+ });
59
+ router.use(validatorErrorTransformer());
60
+ return router;
61
+ }
62
+
63
+ exports.createValidatedOpenApiRouter = createValidatedOpenApiRouter;
9
64
  exports.internal = index;
10
65
  //# sourceMappingURL=index.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
1
+ {"version":3,"file":"index.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';\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 * 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() as ApiRouter<typeof spec>;\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 return router;\n}\n"],"names":["InputError","json","PromiseRouter","OpenApiValidator"],"mappings":";;;;;;;;;;;;;;;;;AAkCA,MAAM,gBAAgB,MAAO,EAAA,CAAA;AAC7B,MAAM,oBAAoB,MAAO,EAAA,CAAA;AAEjC,SAAS,yBAAiD,GAAA;AACxD,EAAA,OAAO,CAAC,KAAA,EAAc,CAAY,EAAA,EAAA,EAAc,IAAuB,KAAA;AACrE,IAAA,IAAA,CAAK,IAAIA,iBAAA,CAAW,KAAM,CAAA,OAAO,CAAC,CAAA,CAAA;AAAA,GACpC,CAAA;AACF,CAAA;AAEO,SAAS,0BAA6B,GAAA;AAC3C,EAAO,OAAA,CAACC,cAAM,CAAA,CAAA;AAChB,CAAA;AASgB,SAAA,4BAAA,CACd,MACA,OAIA,EAAA;AACA,EAAA,MAAM,SAASC,iCAAc,EAAA,CAAA;AAC7B,EAAA,MAAA,CAAO,GAAI,CAAA,CAAA,OAAA,IAAA,IAAA,GAAA,KAAA,CAAA,GAAA,OAAA,CAAS,UAAc,KAAA,0BAAA,EAA4B,CAAA,CAAA;AAY9D,EAAA,MAAA,CAAO,GAAI,CAAA,CAAC,GAAc,EAAA,CAAA,EAAG,IAAS,KAAA;AAMpC,IAAA,MAAM,aAAgB,GAAA,GAAA,CAAA;AACtB,IAAc,aAAA,CAAA,aAAa,IAAI,aAAc,CAAA,OAAA,CAAA;AAC7C,IAAA,aAAA,CAAc,OAAU,GAAA,EAAA,CAAA;AACxB,IAAc,aAAA,CAAA,iBAAiB,IAAI,aAAc,CAAA,WAAA,CAAA;AACjD,IAAA,aAAA,CAAc,cAAc,aAAc,CAAA,GAAA,CAAA;AAC1C,IAAK,IAAA,EAAA,CAAA;AAAA,GACN,CAAA,CAAA;AAGD,EAAO,MAAA,CAAA,GAAA;AAAA,IACLC,kCAAiB,CAAA;AAAA,MACf,gBAAkB,EAAA;AAAA,QAChB,WAAa,EAAA,KAAA;AAAA,QACb,2BAA6B,EAAA,KAAA;AAAA,OAC/B;AAAA,MACA,kBAAoB,EAAA,IAAA;AAAA,MACpB,iBAAmB,EAAA,KAAA;AAAA,MACnB,GAAG,OAAS,IAAA,IAAA,GAAA,KAAA,CAAA,GAAA,OAAA,CAAA,gBAAA;AAAA,MACZ,OAAS,EAAA,IAAA;AAAA,KACV,CAAA;AAAA,GACH,CAAA;AAMA,EAAA,MAAA,CAAO,GAAI,CAAA,CAAC,GAAc,EAAA,CAAA,EAAG,IAAS,KAAA;AACpC,IAAA,MAAM,aAAgB,GAAA,GAAA,CAAA;AACtB,IAAc,aAAA,CAAA,OAAA,GAAU,cAAc,aAAa,CAAA,CAAA;AACnD,IAAc,aAAA,CAAA,WAAA,GAAc,cAAc,iBAAiB,CAAA,CAAA;AAC3D,IAAA,OAAO,cAAc,aAAa,CAAA,CAAA;AAClC,IAAA,OAAO,cAAc,iBAAiB,CAAA,CAAA;AACtC,IAAK,IAAA,EAAA,CAAA;AAAA,GACN,CAAA,CAAA;AAGD,EAAO,MAAA,CAAA,GAAA,CAAI,2BAA2B,CAAA,CAAA;AAEtC,EAAO,OAAA,MAAA,CAAA;AACT;;;;;"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { JSONSchema7, FromSchema } from 'json-schema-to-ts';
2
2
  import { ReferenceObject, OpenAPIObject, ContentObject, RequestBodyObject, ResponseObject, ParameterObject, SchemaObject } from 'openapi3-ts';
3
3
  import core from 'express-serve-static-core';
4
- import { Router } from 'express';
4
+ import { Router, RequestHandler } from 'express';
5
+ import { middleware } from 'express-openapi-validator';
5
6
 
6
7
  /**
7
8
  * This file is meant to hold Immutable overwrites of the values provided by the `openapi3-ts`
@@ -279,11 +280,16 @@ type Filter<T, U> = T extends U ? T : never;
279
280
  */
280
281
  type DocParameter<Doc extends RequiredDoc, Path extends Extract<keyof Doc['paths'], string>, Method extends keyof Doc['paths'][Path], Parameter extends keyof Doc['paths'][Path][Method]['parameters']> = DocOperation<Doc, Path, Method>['parameters'][Parameter] extends ImmutableReferenceObject ? 'parameters' extends ComponentTypes<Doc> ? ComponentRef<Doc, 'parameters', DocOperation<Doc, Path, Method>['parameters'][Parameter]> : never : DocOperation<Doc, Path, Method>['parameters'][Parameter];
281
282
  /**
283
+ * Helper to convert from string to number, used to index arrays and pull out just the indices in the array.
282
284
  * @public
283
285
  */
284
- type DocParameters<Doc extends RequiredDoc, Path extends Extract<keyof Doc['paths'], string>, Method extends keyof Doc['paths'][Path]> = DocOperation<Doc, Path, Method>['parameters'] extends ReadonlyArray<any> ? {
285
- [Index in keyof DocOperation<Doc, Path, Method>['parameters']]: DocParameter<Doc, Path, Method, Index>;
286
- } : never;
286
+ type FromNumberStringToNumber<NumberString extends string | number | symbol> = NumberString extends `${infer R extends number}` ? R : never;
287
+ /**
288
+ * @public
289
+ */
290
+ type DocParameters<Doc extends RequiredDoc, Path extends Extract<keyof Doc['paths'], string>, Method extends keyof Doc['paths'][Path]> = {
291
+ [Index in keyof DocOperation<Doc, Path, Method>['parameters'] as FromNumberStringToNumber<Index>]: DocParameter<Doc, Path, Method, Index>;
292
+ };
287
293
  /**
288
294
  * @public
289
295
  */
@@ -297,7 +303,7 @@ type MapToSchema<Doc extends RequiredDoc, T extends Record<string, ImmutablePara
297
303
  /**
298
304
  * @public
299
305
  */
300
- type ParametersSchema<Doc extends RequiredDoc, Path extends Extract<keyof Doc['paths'], string>, Method extends keyof Doc['paths'][Path], FilterType extends ImmutableParameterObject> = number extends keyof DocParameters<Doc, Path, Method> ? MapToSchema<Doc, FullMap<MapDiscriminatedUnion<Filter<DocParameters<Doc, Path, Method>[number], FilterType>, 'name'>>> : never;
306
+ type ParametersSchema<Doc extends RequiredDoc, Path extends Extract<keyof Doc['paths'], string>, Method extends keyof Doc['paths'][Path], FilterType extends ImmutableParameterObject> = MapToSchema<Doc, FullMap<MapDiscriminatedUnion<Filter<ValueOf<DocParameters<Doc, Path, Method>>, FilterType>, 'name'>>>;
301
307
  /**
302
308
  * @public
303
309
  */
@@ -433,6 +439,7 @@ type index_d_PathObject = PathObject;
433
439
  type index_d_ImmutablePathObject = ImmutablePathObject;
434
440
  type index_d_ImmutableSchemaObject = ImmutableSchemaObject;
435
441
  type index_d_DocParameter<Doc extends RequiredDoc, Path extends Extract<keyof Doc['paths'], string>, Method extends keyof Doc['paths'][Path], Parameter extends keyof Doc['paths'][Path][Method]['parameters']> = DocParameter<Doc, Path, Method, Parameter>;
442
+ type index_d_FromNumberStringToNumber<NumberString extends string | number | symbol> = FromNumberStringToNumber<NumberString>;
436
443
  type index_d_DocParameters<Doc extends RequiredDoc, Path extends Extract<keyof Doc['paths'], string>, Method extends keyof Doc['paths'][Path]> = DocParameters<Doc, Path, Method>;
437
444
  type index_d_ParameterSchema<Doc extends RequiredDoc, Schema extends ImmutableParameterObject['schema']> = ParameterSchema<Doc, Schema>;
438
445
  type index_d_MapToSchema<Doc extends RequiredDoc, T extends Record<string, ImmutableParameterObject>> = MapToSchema<Doc, T>;
@@ -498,6 +505,7 @@ declare namespace index_d {
498
505
  index_d_ImmutablePathObject as ImmutablePathObject,
499
506
  index_d_ImmutableSchemaObject as ImmutableSchemaObject,
500
507
  index_d_DocParameter as DocParameter,
508
+ index_d_FromNumberStringToNumber as FromNumberStringToNumber,
501
509
  index_d_DocParameters as DocParameters,
502
510
  index_d_ParameterSchema as ParameterSchema,
503
511
  index_d_MapToSchema as MapToSchema,
@@ -530,4 +538,16 @@ interface ApiRouter<Doc extends RequiredDoc> extends Router {
530
538
  head: DocRequestMatcher<Doc, this, 'head'>;
531
539
  }
532
540
 
533
- export { ApiRouter, index_d as internal };
541
+ /**
542
+ * Create a new OpenAPI router with some default middleware.
543
+ * @param spec - Your OpenAPI spec imported as a JSON object.
544
+ * @param validatorOptions - `openapi-express-validator` options to override the defaults.
545
+ * @returns A new express router with validation middleware.
546
+ * @public
547
+ */
548
+ declare function createValidatedOpenApiRouter<T extends RequiredDoc>(spec: T, options?: {
549
+ validatorOptions?: Partial<Parameters<typeof middleware>['0']>;
550
+ middleware?: RequestHandler[];
551
+ }): ApiRouter<T>;
552
+
553
+ export { ApiRouter, createValidatedOpenApiRouter, index_d as internal };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@backstage/backend-openapi-utils",
3
3
  "description": "OpenAPI typescript support.",
4
- "version": "0.0.2-next.1",
4
+ "version": "0.0.3-next.0",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "license": "Apache-2.0",
@@ -24,17 +24,21 @@
24
24
  "postpack": "backstage-cli package postpack"
25
25
  },
26
26
  "devDependencies": {
27
- "@backstage/cli": "^0.22.7-next.0"
27
+ "@backstage/cli": "^0.22.10-next.0",
28
+ "supertest": "^6.1.3"
28
29
  },
29
30
  "files": [
30
31
  "dist"
31
32
  ],
32
33
  "dependencies": {
34
+ "@backstage/errors": "^1.2.1",
33
35
  "@types/express": "^4.17.6",
34
36
  "@types/express-serve-static-core": "^4.17.5",
35
37
  "express": "^4.17.1",
38
+ "express-openapi-validator": "^5.0.4",
36
39
  "express-promise-router": "^4.1.0",
37
40
  "json-schema-to-ts": "^2.6.2",
41
+ "lodash": "^4.17.21",
38
42
  "openapi3-ts": "^3.1.2"
39
43
  },
40
44
  "module": "dist/index.esm.js"