Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/@v2/custom-plugins/custom-decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ Here are the key points to understand:
1. The order of plugins as listed in the `plugins` array in `redocly.yaml` configuration file.
1. The order of decorators as defined in the `decorators` object of each plugin.

Note, that the built-in decorators are considered to be part of a special default plugin which is always executed last.

The order in the `decorators` section of `redocly.yaml` **DOES NOT** affect the order in which the decorators are executed.

### Example
Expand Down
128 changes: 128 additions & 0 deletions docs/@v2/decorators/filter-operations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# filter-operations

Keeps only operations where a specified property matches any of the provided values.
All other operations are removed from the API description.

## API design principles

This decorator is useful when you want to publish only a subset of your operations based on specific criteria.

## Configuration

| Option | Type | Description |
| -------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| property | string | **REQUIRED.** The operation property name to filter by. |
| values | [string \| number \| boolean] | **REQUIRED.** Array of values to match. Operations are kept if their property value matches any value in this array. |

### How matching works

If the operation's property is a single value, it must match one of the values in the array.
If the operation's property is an array (like `tags`), the operation is kept if any item in the property array matches any value in the `values` array.
The filter only applies to first-level operation properties that are either primitive types or arrays of primitives.
If there is no such property, the entire operation is removed from the resulting API description.

{% admonition type="info" %}
To remove additional remnants from components, use the `--remove-unused-components` CLI argument or the corresponding decorator.
{% /admonition %}

### Difference from filter-in

While both decorators filter API content, they work at different levels.
**filter-operations** specifically targets operations (HTTP methods like GET, POST, etc.) within paths, while **filter-in** works on any element throughout the API description (paths, parameters, schemas, responses, etc.).
**filter-in** is a more general-purpose filtering tool, but it might affect other document nodes unintentionally (e.g., when filtering by `tags`, you can also affect schemas with the property `tags`).
Also, unlike **filter-operations**, **filter-in** keeps the node if it lacks the property being filtered.

## Examples

### Filter by custom property

In your OpenAPI description:

```yaml
openapi: 3.0.0
paths:
/users:
get:
x-public: true
summary: List public users
post:
x-public: false
summary: Create user (internal only)
/admin:
get:
summary: Admin operation (no x-public property)
```

Configure the decorator to keep only operations where `x-public` is `true`:

```yaml
decorators:
filter-operations:
property: x-public
values: [true]
```

Apply the decorator using the `bundle` command:

```bash
redocly bundle openapi.yaml -o public-api.yaml
```

The resulting API description only contains the `/users` GET operation, as it's the only one with `x-public: true`.
The POST operation on `/users` (where `x-public: false`) and the `/admin` GET operation (which lacks the property) are removed.

### Filter by tags

In your OpenAPI description:

```yaml
openapi: 3.0.0
paths:
/users:
get:
tags: [public, users]
summary: List users
post:
tags: [internal, users]
summary: Create user
/products:
get:
tags: [public, products]
summary: List products
```

Configure the decorator:

```yaml
decorators:
filter-operations:
property: tags
values: [public]
```

The result includes both GET operations (on `/users` and `/products`) because they both have `public` in their tags array.
The POST operation on `/users` is removed because its tags array doesn't contain `public`.

### Filter by operationId

You can also filter operations by explicitly listing specific property values, such as `operationId`:

```yaml
decorators:
filter-operations:
property: operationId
values:
- getUsers
- getProducts
- updateUser
```

## Related decorators

- [filter-in](./filter-in.md) - Filters paths and other elements by property values
- [filter-out](./filter-out.md) - Removes elements matching property values
- [remove-x-internal](./remove-x-internal.md) - Removes items marked as internal

## Resources

- [Decorator source](https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/decorators/common/filters/filter-operations.ts)
1 change: 1 addition & 0 deletions docs/@v2/decorators/filter-out.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ decorators:
## Related decorators

- [filter-in](./filter-in.md)
- [filter-operations](./filter-operations.md)
- [remove-x-internal](./remove-x-internal.md)

## Resources
Expand Down
1 change: 1 addition & 0 deletions docs/@v2/v2.sidebars.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
items:
- page: decorators/filter-in.md
- page: decorators/filter-out.md
- page: decorators/filter-operations.md
- page: decorators/info-description-override.md
- page: decorators/info-override.md
- page: decorators/media-type-examples-override.md
Expand Down
165 changes: 165 additions & 0 deletions packages/core/src/decorators/__tests__/filter-operations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { outdent } from 'outdent';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { bundleDocument } from '../../bundle/bundle-document.js';
import { BaseResolver } from '../../resolve.js';
import { parseYamlToDocument, yamlSerializer } from '../../../__tests__/utils.js';
import { createConfig } from '../../config/index.js';
import { Oas3Types, Oas2Types, bundle } from '../../index.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

describe('oas3 filter-operations', () => {
expect.addSnapshotSerializer(yamlSerializer);

it('should keep only operations with a specific property+value', async () => {
const testDoc = parseYamlToDocument(
outdent`
openapi: 3.0.0
paths:
/users:
get:
x-public: true
summary: List users (public)
post:
summary: Create user (private)
/admin:
get:
x-public: false
summary: Admin only (private)
`
);
const { bundle: res } = await bundleDocument({
document: testDoc,
externalRefResolver: new BaseResolver(),
config: await createConfig({
decorators: { 'filter-operations': { property: 'x-public', values: [true] } },
}),
types: Oas3Types,
});
expect(res.parsed).toMatchInlineSnapshot(`
openapi: 3.0.0
paths:
/users:
get:
x-public: true
summary: List users (public)
components: {}

`);
});

it('should keep only operations with specified operationIds', async () => {
const testDoc = parseYamlToDocument(
outdent`
openapi: 3.0.0
paths:
/foo:
get:
operationId: getFoo
post:
operationId: createFoo
/bar:
get:
operationId: getBar
`
);
const { bundle: res } = await bundleDocument({
document: testDoc,
externalRefResolver: new BaseResolver(),
config: await createConfig({
decorators: {
'filter-operations': {
property: 'operationId',
values: ['createFoo', 'getBar'],
},
},
}),
types: Oas3Types,
});
expect(res.parsed).toMatchInlineSnapshot(`
openapi: 3.0.0
paths:
/foo:
post:
operationId: createFoo
/bar:
get:
operationId: getBar
components: {}
`);
});

it('should keep operations with specified tags', async () => {
const testDoc = parseYamlToDocument(
outdent`
openapi: 3.0.0
paths:
/foo:
get:
tags:
- public
post:
summary: Create Foo (no tags)
/bar:
get:
tags:
- private
`
);
const { bundle: res } = await bundleDocument({
document: testDoc,
externalRefResolver: new BaseResolver(),
config: await createConfig({
decorators: { 'filter-operations': { property: 'tags', values: ['private'] } },
}),
types: Oas3Types,
});
expect(res.parsed).toMatchInlineSnapshot(`
openapi: 3.0.0
paths:
/bar:
get:
tags:
- private
components: {}

`);
});

it('should filter referenced operations and remove unused components', async () => {
const config = await createConfig({
decorators: {
'filter-operations': { property: 'tags', values: ['internal'] },
'remove-unused-components': 'on',
},
});
const { bundle: res } = await bundle({
config,
ref: path.join(__dirname, 'fixtures/filter-operations/openapi.yaml'),
});
expect(res.parsed).toMatchInlineSnapshot(`
openapi: 3.2.0
info:
title: Example API
version: 1.0.0
paths:
/admin:
get:
operationId: adminOp
tags:
- internal
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/admin-schema'
components:
schemas:
admin-schema:
type: object
`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
type: object
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
operationId: adminOp
tags:
- internal
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: admin-schema.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
openapi: 3.2.0
info:
title: Example API
version: 1.0.0
paths:
/users:
get:
tags:
- public
x-public: true
operationId: listUsers
responses:
'200':
description: Success
content:
application/json:
schema:
type: array
items:
$ref: user-schema.yaml
post:
operationId: createUser
tags:
- protected
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: user-schema.yaml
/admin:
get:
$ref: admin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
type: object
Loading
Loading