Skip to content

feat: replace imperative addConnectionFilterOperator with declarative connectionFilterOperatorFactories#810

Open
pyramation wants to merge 32 commits intomainfrom
devin/1773396952-declarative-filter-operators
Open

feat: replace imperative addConnectionFilterOperator with declarative connectionFilterOperatorFactories#810
pyramation wants to merge 32 commits intomainfrom
devin/1773396952-declarative-filter-operators

Conversation

@pyramation
Copy link
Contributor

@pyramation pyramation commented Mar 13, 2026

feat: v5-native connection filter plugin with declarative operator API

Summary

This is a large, multi-part PR that replaces the upstream postgraphile-plugin-connection-filter with a v5-native implementation and migrates all satellite plugins to a new declarative operator registration API. Key changes:

  1. New graphile-connection-filter package — From-scratch v5-native filter plugin with scalar operators, logical operators (and/or/not), computed column filters, relation filters (with index safety), and a declarative custom operator API.
  2. Declarative connectionFilterOperatorFactories — Replaces the imperative build.addConnectionFilterOperator() v4-era API. Satellite plugins now declare operator factories in their preset config, eliminating plugin timing/ordering dependencies.
  3. Satellite plugin migrations — Search (matches), pg_trgm (similarTo/wordSimilarTo), and PostGIS (26 spatial operators) all migrated to the factory API.
  4. Condition → Filter migration — Search and BM25 plugins moved from isPgCondition scope to isPgConnectionFilter. The send-email-link function's GraphQL queries updated from condition: to filter:.
  5. New graphile-pg-trgm-plugin — pg_trgm fuzzy text matching with similarity score fields, orderBy support, and optional index-only mode.
  6. Test suites — Dedicated unit tests for connection-filter (51 tests) and an integration test suite for ConstructivePreset with a "mega query" exercising all 7 plugin types simultaneously.
  7. CI: Docker image → postgres-plus:18 — All CI jobs now use ghcr.io/constructive-io/docker/postgres-plus:18 (includes PostGIS, pgvector, pg_trgm, etc.).

Updates since last revision

  • Fixed array merging bug: graphile-config replaces (not concatenates) arrays when merging presets. Each satellite plugin's connectionFilterOperatorFactories was overwriting the previous one, causing only the last plugin's operators to appear in the schema. The trgm operators (similarTo/wordSimilarTo) were missing as a result.
  • Fix: Factory creator functions are now exported from each satellite plugin (createMatchesOperatorFactory, createTrgmOperatorFactories, createPostgisOperatorFactory), and ConstructivePreset explicitly aggregates all factories in its top-level schema.connectionFilterOperatorFactories.
  • Maintenance note: Any new satellite plugin that adds operator factories must also be added to ConstructivePreset's aggregated array. This is a known coupling — a comment in constructive-preset.ts documents it.

Review & Testing Checklist for Human

This PR is very large and high-risk (architectural). Careful review recommended.

  • Verify factory aggregation is complete: Check that ConstructivePreset's connectionFilterOperatorFactories array includes factories for all satellite plugins. If a factory is missing, its operators will silently not appear in the schema. Compare against the extends array to confirm nothing is missed.
  • Watch for parameter duplication: createMatchesOperatorFactory('FullText', 'english') in ConstructivePreset duplicates the defaults from PgSearchPreset({ pgSearchPrefix: 'fullText' }). If someone changes the search preset options, they must also update this factory call — or the matches operator will target the wrong scalar type. Consider whether these should be derived from a single source of truth.
  • Verify PostGIS operator parity: The PostGIS filter plugin (src/plugin.ts) was 95% rewritten. Compare the 26 operator specs (13 function-based, 13 SQL-based) against the old version to confirm identical SQL generation. The sql.raw usage for SQL operators is flagged by pg-sql2 — confirm this is safe.
  • Test declarative operator API end-to-end: Deploy to staging and verify that filter operators from all 3 satellite plugins (search matches, trgm similarTo/wordSimilarTo, PostGIS spatial operators) appear in the schema and work correctly. Run the integration test "mega query" manually against staging.
  • Test send-email-link function: The condition:filter: migration in /functions/send-email-link/src/index.ts changes query semantics. Trigger a send-email flow in staging and confirm it still works.
  • Verify relation filter index safety: The new connectionFilterRelationsRequireIndex: true default restricts relation filters to indexed FKs. Check that legitimate relation filter queries still work and that queries on non-indexed FKs are correctly blocked.

Test Plan

  1. Deploy to staging
  2. Verify schema introspection shows all filter operators:
    • Search: matches on FullTextFilter
    • pg_trgm: similarTo and wordSimilarTo on StringFilter
    • PostGIS: 26 spatial operators on GeometryFilter/GeographyFilter
  3. Run a GraphQL query using each filter type:
    • Scalar: users(filter: { email: { equalTo: "test@example.com" } })
    • Search: items(filter: { description: { matches: "widget" } })
    • pg_trgm: locations(filter: { name: { similarTo: { value: "cenral prk", threshold: 0.3 } } })
    • PostGIS: places(filter: { geom: { intersects: $bbox } })
    • Relation: orders(filter: { clientByClientId: { name: { includes: "acme" } } })
  4. Trigger send-email-link function and confirm emails are sent
  5. Check Sentry for filter-related errors over 24h

Notes

  • Lockfile noise: pnpm-lock.yaml has extensive formatting changes (likely pnpm version difference). The actual dependency changes are minimal (new graphile-connection-filter and graphile-pg-trgm-plugin workspace packages).
  • Testing limitations: Integration tests (search, trgm, connection-filter e2e) require PostgreSQL and couldn't be run locally. Full test coverage verified in CI.
  • Architecture change: This is a major shift from imperative → declarative operator registration. The new connectionFilterOperatorFactories system has zero production usage. While the migration is thorough, there's inherent risk in untested architectural changes.
  • Maintenance footgun: If a new satellite plugin with custom operators is added, its factory creator must also be added to ConstructivePreset's aggregated connectionFilterOperatorFactories array. Otherwise, its operators will silently not appear when using ConstructivePreset. This is documented in constructive-preset.ts.

Link to Devin Session: https://app.devin.ai/sessions/cf88f3fd383b4421a5169ed01612899d
Requested by: @pyramation

Implements a from-scratch PostGraphile v5 native connection filter plugin,
replacing the upstream postgraphile-plugin-connection-filter dependency.

New package: graphile/graphile-connection-filter/

Plugin architecture (7 plugins):
- ConnectionFilterInflectionPlugin: filter type naming conventions
- ConnectionFilterTypesPlugin: registers per-table and per-scalar filter types
- ConnectionFilterArgPlugin: injects filter arg on connections via applyPlan
- ConnectionFilterAttributesPlugin: adds per-column filter fields
- ConnectionFilterOperatorsPlugin: standard/sort/pattern/jsonb/inet/array/range operators
- ConnectionFilterCustomOperatorsPlugin: addConnectionFilterOperator API for satellite plugins
- ConnectionFilterLogicalOperatorsPlugin: and/or/not logical composition

Key features:
- Full v5 native: uses Grafast planning, PgCondition, codec system, behavior registry
- EXPORTABLE pattern for schema caching
- Preserves addConnectionFilterOperator API for PostGIS, search, pgvector, textsearch plugins
- No relation filter plugins (simplifies configuration vs upstream)
- Preset factory: ConnectionFilterPreset(options)

Also updates graphile-settings to use the new workspace package.
…Operator

filterType is for table-level filter types (UserFilter), while filterFieldType
is for scalar operator types (StringFilter). Satellite plugins pass scalar type
names, so the lookup must use filterFieldType to match the registration in
ConnectionFilterTypesPlugin. Previously worked by coincidence since both
inflections produce the same output, but would silently fail if a consumer
overrode one inflection but not the other.
Adds computed column filter support — allows filtering on PostgreSQL functions
that take a table row as their first argument and return a scalar.

Controlled by connectionFilterComputedColumns schema option. The preset factory
includes the plugin only when the option is truthy (default in preset: true,
but constructive-preset sets it to false).
- Remove phantom postgraphile-plugin-connection-filter dep from graphile-pgvector-plugin (never used)
- Remove phantom postgraphile-plugin-connection-filter dep from graphile-pg-textsearch-plugin (never used)
- Update graphile-plugin-connection-filter-postgis to use graphile-connection-filter workspace dep with typed imports
- Update graphile-search-plugin to use graphile-connection-filter workspace dep with typed imports
- Replace (build as any).addConnectionFilterOperator casts with properly typed build.addConnectionFilterOperator
…on-filter

- Update search plugin, pgvector, and postgis test files to import from
  graphile-connection-filter instead of postgraphile-plugin-connection-filter
- Use ConnectionFilterPreset() factory instead of PostGraphileConnectionFilterPreset
- Import ConnectionFilterOperatorSpec type from graphile-connection-filter
- Fix smart quote characters in filter descriptions to match existing snapshots
…ion filter tests

- Add graphile-connection-filter as devDependency in graphile-pgvector-plugin
  (test file imports ConnectionFilterPreset but package had no dependency)
- Skip connectionFilterRelations tests in search plugin (relation filters
  are intentionally not included in the v5-native plugin; they were disabled
  in production via disablePlugins with the old plugin)
…toggle

- ConnectionFilterForwardRelationsPlugin: filter by FK parent relations
- ConnectionFilterBackwardRelationsPlugin: filter by backward relations (one-to-one + one-to-many with some/every/none)
- connectionFilterRelations toggle in preset (default: false)
- Un-skip relation filter tests in search plugin
- Updated augmentations, types, and exports
… at runtime

The preset factory now always includes relation plugins in the plugin list.
Each plugin checks build.options.connectionFilterRelations at runtime and
early-returns if disabled. This allows the toggle to be set by any preset
in the chain, not just the ConnectionFilterPreset() call.
Enables relation filter fields in the production schema:
- Forward: filter by FK parent (e.g. clientByClientId on OrderFilter)
- Backward: filter by children with some/every/none
- Codegen will pick up the new filter fields automatically
- Search plugin: isPgCondition → isPgConnectionFilter scope
- BM25 plugin: isPgCondition → isPgConnectionFilter scope
- Disable PgConditionArgumentPlugin and PgConditionCustomFieldsPlugin in preset
- Update all tests from condition: {...} to filter: {...}
- Add graphile-connection-filter devDependency to BM25 plugin
- Update search plugin graceful degradation tests to use filter

BREAKING CHANGE: The condition argument has been removed entirely.
All filtering now uses the filter argument exclusively.
- Search plugin plugin.test.ts: condition → filter syntax, add ConnectionFilterPreset
- Server-test: condition → filter in query with equalTo operator
- Clear stale snapshots (schema-snapshot, introspection) for regeneration
- Search plugin: update snapshot keys to match renamed filter-based tests
- Schema snapshot: remove all condition arguments and XxxCondition input types
- Introspection snapshot: remove condition arg and UserCondition type
- Kept conditionType in _meta schema (unrelated to deprecated condition arg)
… behavior for pgCodecRelation, update schema snapshot with relation filter types
…y filter at applyPlan level

Top-level empty filter {} is now treated as 'no filter' (skipped) instead of
throwing an error. Nested empty objects in and/or/not and relation filters are
still rejected. This removes the need for the connectionFilterAllowEmptyObjectInput
workaround in pgvector tests.
- Extract shared getQueryBuilder utility into graphile-connection-filter/src/utils.ts
- Remove duplicate getQueryBuilder from search, BM25, and pgvector plugins
- Replace (build as any).dataplanPg with build.dataplanPg (already typed on Build)
- Replace (build as any).behavior with build.behavior (already typed on Build)
- Replace (build as any).input.pgRegistry with build.input.pgRegistry (already typed)
- Remove scope destructuring as any casts (pgCodec already typed on ScopeInputObject)
- Add pgCodec comment to augmentations.ts noting it's already declared by graphile-build-pg
- Export getQueryBuilder from graphile-connection-filter for satellite plugin use
Adds index safety check for relation filter fields. When enabled (default: true),
relation filter fields are only created for FKs with supporting indexes.
This prevents generating EXISTS subqueries that would cause sequential scans
on large tables.

Uses PgIndexBehaviorsPlugin's existing relation.extensions.isIndexed metadata
which is set at gather time. The check runs at schema build time with zero
runtime cost.

Applied to both forward and backward relation filter plugins.
Comprehensive test coverage using graphile-test infrastructure:
- Scalar operators: equalTo, notEqualTo, distinctFrom, isNull, in/notIn,
  lessThan, greaterThan, like, iLike, includes, startsWith, endsWith
- Logical operators: and, or, not, nested combinations
- Relation filters: forward (child->parent), backward one-to-one,
  backward one-to-many (some/every/none), exists fields
- Computed column filters
- Schema introspection: filter types, operator fields, relation fields
- Options toggles: connectionFilterRelations, connectionFilterComputedColumns,
  connectionFilterLogicalOperators, connectionFilterAllowedOperators,
  connectionFilterOperatorNames

Also adds graphile/graphile-connection-filter to CI matrix (41 jobs).
Exercises multiple plugins working together in a single test database:
- Connection filter (scalar operators, logical operators, relation filters)
- PostGIS spatial filters (geometry column)
- pgvector (vector column, search function, distance ordering)
- tsvector search plugin (fullText matches, rank, orderBy)
- BM25 search (pg_textsearch body index, score, orderBy)
- Kitchen sink queries combining multiple plugins

34 test cases across 8 describe blocks, all passing locally.
Added postgres-plus CI job for tests requiring PostGIS/pgvector/pg_textsearch.
… test

The mega query now exercises all SIX plugin types in a single filter:
- tsvector (fullTextTsv)
- BM25 (bm25Body)
- relation filter (category name)
- scalar filter (isActive)
- pgvector (vectorEmbedding nearby)
- PostGIS (geom intersects polygon bbox)

Also validates returned coordinates fall within the bounding box.
New package: graphile-pg-trgm-plugin — a PostGraphile v5 plugin for pg_trgm
trigram-based fuzzy text matching. Zero config, works on any text column.

Features:
- similarTo / wordSimilarTo filter operators on StringFilter
- trgm<Column> direct filter fields on connection filter types
- <column>Similarity computed score fields (0-1, null when inactive)
- SIMILARITY_<COLUMN>_ASC/DESC orderBy enum values
- TrgmSearchPreset for easy composition into presets
- connectionFilterTrgmRequireIndex option (default: false)
- 14 dedicated tests + integrated into mega query as 7th plugin type

Mega query now exercises ALL 7 plugin types in one GraphQL query:
tsvector + BM25 + pgvector + PostGIS + pg_trgm + relation filter + scalar
Updated introspection and SDL snapshots to include new fields from
TrgmSearchPlugin: similarTo/wordSimilarTo operators on StringFilter,
*Similarity computed fields, trgm* filter fields, and SIMILARITY_*
orderBy enum values.
- orderBy: [BM25_BODY_SCORE_ASC, SIMILARITY_NAME_DESC] demonstrates
  multi-signal relevance ranking in a single query
- Added comprehensive JSDoc explaining all 7 plugin types, the 2-phase
  meta system, and ORDER BY priority semantics
- Inline GraphQL comments explain each filter and score field
- Assertion verifies BM25 ASC ordering (primary sort)
- Documents important subtlety: ORDER BY priority follows schema field
  processing order, not the orderBy array order
… connectionFilterOperatorFactories

Migrates all 3 satellite plugins (search, pg_trgm, PostGIS) from the
imperative build.addConnectionFilterOperator() API to the new declarative
connectionFilterOperatorFactories preset configuration.

Changes:
- Core: Add ConnectionFilterOperatorFactory type, ConnectionFilterOperatorRegistration
  interface, and connectionFilterOperatorFactories schema option
- Core: ConnectionFilterCustomOperatorsPlugin processes factories during init hook
- Search plugin: Extract matches operator into createMatchesOperatorFactory
- pg_trgm plugin: Extract similarTo/wordSimilarTo into createTrgmOperatorFactories
- PostGIS plugin: Refactor entire plugin into createPostgisOperatorFactory factory
- PostGIS tests: Rewrite to call factory directly instead of mocking init hook
- README: Update custom operator docs to show declarative API
- Remove all imperative addConnectionFilterOperator calls from satellite plugins
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

…t to fix array merging

graphile-config replaces (not concatenates) arrays when merging presets,
so each satellite plugin's connectionFilterOperatorFactories was overwriting
the previous one. Only the last preset's factories survived.

Fix: export factory creators from each satellite plugin and aggregate all
factories explicitly in ConstructivePreset's top-level schema options.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant