Skip to content

Support !-prefix exclusions in filter entries #216

Description

@petehauge

Problem or use case

Wildcard support in filters (#48) lets users concisely include large sets of resources by naming convention:

apiNames:
  - 'prod-*'

But there's no way to say "include everything matching this pattern except these specific ones." Users have hit this in practice — e.g. "include all prod-* APIs but not prod-legacy-billing," or "extract all backends except the shared infra ones."

The current workarounds are all bad:

  • Enumerate every name explicitly (defeats the purpose of wildcards).
  • Split into multiple runs with different filter files and stitch results.
  • Post-process artifacts to remove unwanted ones (same problem the filter was meant to solve).

Proposed solution

Add a negation operator ! as the first character of any filter entry. A leading ! marks the entry as an exclusion rather than an inclusion. Exclusions are always evaluated after inclusions for the same resource type: a resource is included iff at least one inclusion matches it and no exclusion matches it.

Examples:

apiNames:
  - 'prod-*'
  - '!prod-legacy-billing'      # exact-name exclusion
  - '!prod-*-deprecated'        # wildcard exclusion

backendNames:
  - '*'
  - '!shared-monitoring'
  - '!shared-*-infra'

namedValueNames:
  - '*'
  - '!keyvault-*'               # never publish/extract KV-backed named values

Semantics:

  • ! must be the first character of the entry to be interpreted as negation. foo!bar is a literal name.
  • The remainder of the string is a normal filter entry: exact name or wildcard pattern (*, ?), case-insensitive, same as today.
  • Evaluation order: for each candidate resource, check inclusions first; if none match, exclude. If any inclusion matches, then check exclusions; if any exclusion matches, exclude. Otherwise include.
  • A filter list containing only exclusions is treated as "include-all, then subtract" — equivalent to prepending an implicit *. This is a convenience so users can write apiNames: ['!prod-legacy-billing'] to mean "everything except that one."
  • Applies uniformly to all filter fields that already accept string entries (apiNames, productNames, backendNames, namedValueNames, etc.) and to sub-filter fields inside apiSubFilters and workspaceSubFilters.
  • Case-insensitive matching for both inclusion and exclusion, matching current filter behavior.
  • No escape hatch needed. Verified against APIM resource name constraints: every filterable resource type (API, Backend, Product, NamedValue, PolicyFragment, Gateway, Workspace, Subscription, Tag, VersionSet, Logger, Diagnostic, Schema, Documentation) requires a name matching a pattern that starts with (and only contains) alphanumeric characters, hyphens, and — for policy fragments — underscores. ! cannot appear anywhere in a valid APIM resource name, so a leading ! in a filter entry is unambiguous by construction.

Interaction with existing features:

  • API revision-suffix handling: exclusions match the same way inclusions do (root name matched with revision suffix stripped, per current filter-service.ts behavior).
  • Parent → child inheritance: if a parent (Api, Product, Gateway, Workspace) is excluded, its children are excluded too, matching how inclusions inherit today.
  • Object-form entries (nested sub-filters): the object form uses the resource name as a key, so ! can be a leading character of that key. Same rules apply.

Applies to both:

Implementation notes (non-binding)

  • Localize the change to src/services/filter-service.ts. The public shouldIncludeResource / filterResources API stays the same; only matchesFilter and its wildcard/exact-match helpers need to understand negation.
  • Suggested internal shape: partition each allowlist once at load time (or lazily on first use) into { includes: string[]; excludes: string[] }, then decide inclusion as (includes.length === 0 || any(includes match)) && !any(excludes match).
  • No config-schema change needed — !-prefixed strings are still valid string entries. The loader in src/lib/config-loader.ts doesn't need to know about the operator.
  • Documentation: extend the wildcard docs added in Add wildcard/pattern matching support to filter configuration #48 with a negation section and 2–3 worked examples.

Out of scope

  • More expressive expression languages (regex, boolean AND/OR, grouping). If users want that later, it's a separate design conversation.
  • Changing the shape of FilterConfig or introducing a new field for exclusions. The ! prefix is intentionally minimal and stays within the current schema.

Acceptance criteria

  • Filter entries whose first character is ! are treated as exclusions.
  • ! supports both exact names and wildcard patterns (!foo, !prod-*).
  • A resource is included iff at least one inclusion matches (or the list contains only exclusions) AND no exclusion matches.
  • Exclusions respect existing filter semantics: case-insensitive, API revision-suffix stripping, parent→child inheritance.
  • Works in top-level filter fields (apiNames, backendNames, etc.) and in apiSubFilters / workspaceSubFilters string lists.
  • Works for apiops extract --filter today and for apiops publish --filter once Add --filter flag to subset resources during publish #215 lands (no publish-specific changes required).
  • Unit tests in filter-service cover: pure-exclusion list, mixed inclusion + exclusion, wildcard exclusion, exclusion of API root vs. revision, parent exclusion cascading to children.
  • Docs updated with negation examples alongside the existing wildcard docs.

Affected command

apiops extract, apiops publish (once #215 lands)

Metadata

Metadata

Assignees

Labels

close:fixedFixed by a previous PR or releasetype:featureNew capability

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions