Skip to content

Commit

Permalink
feat: #467 support for URI path params (#491)
Browse files Browse the repository at this point in the history
* feat: #467 support for URI path params

* update README

* update README

* feat: wildcard routes not found
  • Loading branch information
cdimascio committed Dec 19, 2020
1 parent 3e6a26b commit 0f7fcda
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 9 deletions.
31 changes: 30 additions & 1 deletion README.md
Expand Up @@ -561,7 +561,7 @@ Determines whether the validator should validate requests.
responses:
200:
description: success
```
```

**coerceTypes:**

Expand Down Expand Up @@ -774,10 +774,13 @@ Defines a regular expression or function that determines whether a path(s) shoul

The following ignores any path that ends in `/pets` e.g. `/v1/pets`.
As a regular expression:

```
ignorePaths: /.*\/pets$/
```

or as a function:

```
ignorePaths: (path) => path.endsWith('/pets')
```
Expand Down Expand Up @@ -1048,6 +1051,31 @@ module.exports = app;
## FAQ
**Q:** How do I match paths, like those described in RFC-6570?
**A:** OpenAPI 3.0 does not support RFC-6570. That said, we provide a minimalistic mechanism that conforms syntactically to OpenAPI 3 and accomplishes a common use case. For example, matching file paths and storing the matched path in `req.params`
Using the following OpenAPI 3.x defintion
```yaml
/files/{path}*:
get:
parameters:
- name: path
in: path
required: true
schema:
type: string
```
With the following Express route defintion
```javascript
app.get(`/files/:path(*)`, (req, res) => { /* do stuff */ }`
```

A path like `/files/some/long/path` will pass validation. The Express `req.params.path` property will hold the value `some/long/path`.

**Q:** What happened to the `securityHandlers` property?

**A:** In v3, `securityHandlers` have been replaced by `validateSecurity.handlers`. To use v3 security handlers, move your existing security handlers to the new property. No other change is required. Note that the v2 `securityHandlers` property is supported in v3, but deprecated
Expand Down Expand Up @@ -1172,6 +1200,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
Expand Down
12 changes: 11 additions & 1 deletion src/framework/openapi.spec.loader.ts
Expand Up @@ -98,6 +98,16 @@ export class OpenApiSpecLoader {
}

private toExpressParams(part: string): string {
return part.replace(/\{([^}]+)}/g, ':$1');
// substitute wildcard path with express equivalent
// {/path} => /path(*) <--- RFC 6570 format (not supported by openapi)
// const pass1 = part.replace(/\{(\/)([^\*]+)(\*)}/g, '$1:$2$3');

// instead create our own syntax that is compatible with express' pathToRegex
// /{path}* => /:path*)
// /{path}(*) => /:path*)
const pass1 = part.replace(/\/{([^\*]+)}\({0,1}(\*)\){0,1}/g, '/:$1$2');
// substitute params with express equivalent
// /path/{id} => /path/:id
return pass1.replace(/\{([^}]+)}/g, ':$1');
}
}
27 changes: 20 additions & 7 deletions src/middlewares/openapi.request.validator.ts
Expand Up @@ -114,9 +114,22 @@ export class RequestValidator {

return (req: OpenApiRequest, res: Response, next: NextFunction): void => {
const openapi = <OpenApiRequestMetadata>req.openapi;
const hasPathParams = Object.keys(openapi.pathParams).length > 0;
const pathParams = Object.keys(openapi.pathParams);
const hasPathParams = pathParams.length > 0;

if (hasPathParams) {
// handle wildcard path param syntax
if (openapi.expressRoute.endsWith('*')) {
// if we have an express route /data/:p*, we require a path param, p
// if the p param is empty, the user called /p which is not found
// if it was found, it would match a different route
if (pathParams.filter((p) => openapi.pathParams[p]).length === 0) {
throw new NotFound({
path: req.path,
message: 'not found',
});
}
}
req.params = openapi.pathParams ?? req.params;
}

Expand Down Expand Up @@ -189,13 +202,13 @@ export class RequestValidator {
if (discriminator) {
const { options, property, validators } = discriminator;
const discriminatorValue = req.body[property]; // TODO may not alwasy be in this position
if (options.find(o => o.option === discriminatorValue)) {
if (options.find((o) => o.option === discriminatorValue)) {
return validators[discriminatorValue];
} else {
throw new BadRequest({
path: req.path,
message: `'${property}' should be equal to one of the allowed values: ${options
.map(o => o.option)
.map((o) => o.option)
.join(', ')}.`,
});
}
Expand All @@ -213,7 +226,7 @@ export class RequestValidator {
keys.push(key);
}
const knownQueryParams = new Set(keys);
whiteList.forEach(item => knownQueryParams.add(item));
whiteList.forEach((item) => knownQueryParams.add(item));
const queryParams = Object.keys(query);
const allowedEmpty = schema.allowEmptyValue;
for (const q of queryParams) {
Expand Down Expand Up @@ -324,12 +337,12 @@ class Security {
): string[] {
return usedSecuritySchema && securitySchema
? usedSecuritySchema
.filter(obj => Object.entries(obj).length !== 0)
.map(sec => {
.filter((obj) => Object.entries(obj).length !== 0)
.map((sec) => {
const securityKey = Object.keys(sec)[0];
return <SecuritySchemeObject>securitySchema[securityKey];
})
.filter(sec => sec?.type === 'apiKey' && sec?.in == 'query')
.filter((sec) => sec?.type === 'apiKey' && sec?.in == 'query')
.map((sec: ApiKeySecurityScheme) => sec.name)
: [];
}
Expand Down
58 changes: 58 additions & 0 deletions test/resources/wildcard.path.params.yaml
@@ -0,0 +1,58 @@
openapi: 3.0.1
info:
title: dummy api
version: 1.0.0
servers:
- url: /v1

paths:
/d1/{id}:
get:
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
200:
description: dummy response
content: {}

/d2/{path}(*):
get:
parameters:
- name: path
in: path
required: true
schema:
type: string
responses:
200:
description: dummy response
content: {}

/d3/{path}*:
get:
parameters:
- name: path
in: path
required: true
schema:
type: string
- name: qp
in: query
required: true
schema:
type: string
responses:
200:
description: dummy response
content: {}

/d3:
get:
responses:
200:
description: dummy response
content: {}
86 changes: 86 additions & 0 deletions test/wildcard.path.params.spec.ts
@@ -0,0 +1,86 @@
import * as path from 'path';
import { expect } from 'chai';
import * as request from 'supertest';
import { createApp } from './common/app';

describe('wildcard path params', () => {
let app = null;

before(async () => {
const apiSpec = path.join('test', 'resources', 'wildcard.path.params.yaml');
app = await createApp(
{
apiSpec,
},
3001,
(app) => {
app
.get(`${app.basePath}/d1/:id`, (req, res) =>
res.json({
...req.params,
}),
)
.get(`${app.basePath}/d2/:path(*)`, (req, res) =>
res.json({
...req.params,
}),
)
.get(`${app.basePath}/d3/:path(*)`, (req, res) =>
res.json({
...req.params,
}),
)
.get(`${app.basePath}/d3`, (req, res) =>
res.json({
success: true,
}),
);
},
);
});

after(() => app.server.close());

it('should allow path param without wildcard', async () =>
request(app)
.get(`${app.basePath}/d1/my-id`)
.expect(200)
.then((r) => {
expect(r.body.id).to.equal('my-id');
}));

it('should allow path param with slashes "/" using wildcard', async () =>
request(app)
.get(`${app.basePath}/d2/some/long/path`)
.expect(200)
.then((r) => {
expect(r.body.path).to.equal('some/long/path');
}));

it('should return not found if no path is specified', async () =>
request(app).get(`${app.basePath}/d2`).expect(404));

it('should return 200 when wildcard path includes all required params', async () =>
request(app)
.get(`${app.basePath}/d3/long/path/file.csv`)
.query({
qp: 'present',
})
.expect(200));

it('should 400 when wildcard path is missing a required query param', async () =>
request(app)
.get(`${app.basePath}/d3/long/path/file.csv`)
.expect(400)
.then((r) => {
expect(r.body.message).to.include('required');
}));

it('should return 200 if root of an existing wildcard route is defined', async () =>
request(app)
.get(`${app.basePath}/d3`)
.expect(200)
.then((r) => {
expect(r.body.success).to.be.true;
}));
});

0 comments on commit 0f7fcda

Please sign in to comment.