Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
616dee9
feat: add v5-native graphile-connection-filter plugin
pyramation Mar 12, 2026
eac0208
fix: use filterFieldType instead of filterType in addConnectionFilter…
pyramation Mar 12, 2026
b1ca75d
feat: add ConnectionFilterComputedAttributesPlugin for feature parity
pyramation Mar 12, 2026
ac3d641
refactor: clean up satellite plugin dependencies on connection filter
pyramation Mar 12, 2026
5f77654
Merge origin/main into devin/1773292005-graphile-connection-filter-v5
pyramation Mar 12, 2026
b2e07e9
fix: update test imports and description quotes for graphile-connecti…
pyramation Mar 12, 2026
5744b8b
fix: add graphile-connection-filter devDep to pgvector and skip relat…
pyramation Mar 12, 2026
e8af929
feat: add optional relation filter plugins (forward + backward) with …
pyramation Mar 12, 2026
b4ff740
fix: always include relation plugins, check connectionFilterRelations…
pyramation Mar 12, 2026
28b23c4
feat: enable connectionFilterRelations in constructive preset
pyramation Mar 12, 2026
76f35f5
feat: deprecate condition argument, move search + BM25 plugins to filter
pyramation Mar 13, 2026
10ed4c4
fix: update tests and snapshots for condition deprecation
pyramation Mar 13, 2026
eb9e5b8
fix: provide correct snapshot content for CI (no condition fields)
pyramation Mar 13, 2026
5207254
fix: migrate remaining condition queries to filter, register filterBy…
pyramation Mar 13, 2026
8191c00
fix: allow empty filter objects in pgvector test, add relation filter…
pyramation Mar 13, 2026
e6bc127
fix: delete snapshot to let Jest regenerate with correct relation fil…
pyramation Mar 13, 2026
11f8873
fix: correct schema snapshot with relation filter types, smart quotes…
pyramation Mar 13, 2026
48c8332
fix: add missing blank line between UsersEdge and UserOrderBy in snap…
pyramation Mar 13, 2026
f21d0f0
fix: remove connectionFilterAllowEmptyObjectInput option, handle empt…
pyramation Mar 13, 2026
72ac8fe
feat: tighten TypeScript types in satellite filter plugins
pyramation Mar 13, 2026
58c638a
feat: add connectionFilterRelationsRequireIndex option
pyramation Mar 13, 2026
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
6 changes: 3 additions & 3 deletions functions/send-email-link/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const app = createJobApp();

const GetUser = gql`
query GetUser($userId: UUID!) {
users(condition: { id: $userId }, first: 1) {
users(filter: { id: { equalTo: $userId } }, first: 1) {
nodes {
username
displayName
Expand All @@ -26,7 +26,7 @@ const GetUser = gql`

const GetDatabaseInfo = gql`
query GetDatabaseInfo($databaseId: UUID!) {
databases(condition: { id: $databaseId }, first: 1) {
databases(filter: { id: { equalTo: $databaseId } }, first: 1) {
nodes {
sites {
nodes {
Expand All @@ -43,7 +43,7 @@ const GetDatabaseInfo = gql`
theme
}
}
siteModules(condition: { name: "legal_terms_module" }) {
siteModules(filter: { name: { equalTo: "legal_terms_module" } }) {
nodes {
data
}
Expand Down
40 changes: 40 additions & 0 deletions graphile/graphile-connection-filter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# graphile-connection-filter

A PostGraphile v5 native connection filter plugin for the Constructive monorepo.

Adds advanced filtering capabilities to connection and list fields, including:

- Per-table filter types (e.g. `UserFilter`)
- Per-scalar operator types (e.g. `StringFilter`, `IntFilter`)
- Standard operators: `equalTo`, `notEqualTo`, `isNull`, `in`, `notIn`, etc.
- Sort operators: `lessThan`, `greaterThan`, etc.
- Pattern matching: `includes`, `startsWith`, `endsWith`, `like` + case-insensitive variants
- Type-specific operators: JSONB, hstore, inet, array, range
- Logical operators: `and`, `or`, `not`
- Custom operator API: `addConnectionFilterOperator` for satellite plugins

## Usage

```typescript
import { ConnectionFilterPreset } from 'graphile-connection-filter';

const preset: GraphileConfig.Preset = {
extends: [
ConnectionFilterPreset(),
],
};
```

## Custom Operators

Satellite plugins can register custom operators during the `init` hook:

```typescript
const addConnectionFilterOperator = (build as any).addConnectionFilterOperator;
if (typeof addConnectionFilterOperator === 'function') {
addConnectionFilterOperator('MyType', 'myOperator', {
description: 'My custom operator',
resolve: (sqlIdentifier, sqlValue) => sql`${sqlIdentifier} OP ${sqlValue}`,
});
}
```
55 changes: 55 additions & 0 deletions graphile/graphile-connection-filter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "graphile-connection-filter",
"version": "1.0.0",
"description": "PostGraphile v5 native connection filter plugin - adds advanced filtering to connections",
"author": "Constructive <developers@constructive.io>",
"homepage": "https://github.com/constructive-io/constructive",
"license": "MIT",
"main": "index.js",
"module": "esm/index.js",
"types": "index.d.ts",
"scripts": {
"clean": "makage clean",
"prepack": "npm run build",
"build": "makage build",
"build:dev": "makage build --dev",
"lint": "eslint . --fix",
"test": "jest",
"test:watch": "jest --watch"
},
"publishConfig": {
"access": "public",
"directory": "dist"
},
"repository": {
"type": "git",
"url": "https://github.com/constructive-io/constructive"
},
"keywords": [
"postgraphile",
"graphile",
"constructive",
"plugin",
"postgres",
"graphql",
"filter",
"connection-filter",
"v5"
],
"bugs": {
"url": "https://github.com/constructive-io/constructive/issues"
},
"devDependencies": {
"@types/node": "^22.19.11",
"makage": "^0.1.10"
},
"peerDependencies": {
"@dataplan/pg": "1.0.0-rc.5",
"graphile-build": "5.0.0-rc.4",
"graphile-build-pg": "5.0.0-rc.5",
"graphile-config": "1.0.0-rc.5",
"graphql": "^16.9.0",
"pg-sql2": "5.0.0-rc.4",
"postgraphile": "5.0.0-rc.7"
}
}
113 changes: 113 additions & 0 deletions graphile/graphile-connection-filter/src/augmentations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* TypeScript namespace augmentations for graphile-connection-filter.
*
* These extend the Graphile type system so that our custom inflection methods,
* build properties, scope properties, schema options, and behaviors are
* recognized by the TypeScript compiler.
*/

import 'graphile-build';
import 'graphile-build-pg';
import type { ConnectionFilterOperatorSpec, ConnectionFilterOperatorsDigest, PgConnectionFilterOperatorsScope } from './types';

declare global {
namespace GraphileBuild {
interface Inflection {
/** Filter type name for a table, e.g. "UserFilter" */
filterType(this: Inflection, typeName: string): string;
/** Filter field type name for a scalar, e.g. "StringFilter" */
filterFieldType(this: Inflection, typeName: string): string;
/** Filter field list type name for an array scalar, e.g. "StringListFilter" */
filterFieldListType(this: Inflection, typeName: string): string;
/** Forward relation field name (passthrough) */
filterSingleRelationFieldName(this: Inflection, fieldName: string): string;
/** Forward relation exists field name, e.g. "clientByClientIdExists" */
filterForwardRelationExistsFieldName(this: Inflection, relationFieldName: string): string;
/** Backward single relation field name (passthrough) */
filterSingleRelationByKeysBackwardsFieldName(this: Inflection, fieldName: string): string;
/** Backward single relation exists field name */
filterBackwardSingleRelationExistsFieldName(this: Inflection, relationFieldName: string): string;
/** Backward many relation field name (passthrough) */
filterManyRelationByKeysFieldName(this: Inflection, fieldName: string): string;
/** Backward many relation exists field name */
filterBackwardManyRelationExistsFieldName(this: Inflection, relationFieldName: string): string;
/** Many filter type name, e.g. "ClientToManyOrderFilter" */
filterManyType(this: Inflection, table: any, foreignTable: any): string;
}

interface Build {
/** Returns the operator digest for a given codec, or null if not filterable */
connectionFilterOperatorsDigest(codec: any): ConnectionFilterOperatorsDigest | null;
/** Escapes LIKE wildcard characters (% and _) */
escapeLikeWildcards(input: unknown): string;
/** Registers a custom filter operator (used by satellite plugins) */
addConnectionFilterOperator(
typeNameOrNames: string | string[],
filterName: string,
spec: ConnectionFilterOperatorSpec
): void;
/** Internal filter operator registry keyed by filter type name */
[key: symbol]: any;
}

interface ScopeInputObject {
/** True if this is a table-level connection filter type (e.g. UserFilter) */
isPgConnectionFilter?: boolean;
// pgCodec is already declared by graphile-build-pg on ScopeInputObject
/** Operator type scope data (present on scalar filter types like StringFilter) */
pgConnectionFilterOperators?: PgConnectionFilterOperatorsScope;
/** Foreign table resource (used by many filter types) */
foreignTable?: any;
/** True if this is a many-relation filter type (e.g. ClientToManyOrderFilter) */
isPgConnectionFilterMany?: boolean;
}

interface ScopeInputObjectFieldsField {
/** True if this field is an attribute-based filter field */
isPgConnectionFilterField?: boolean;
/** True if this field is a filter operator (e.g. equalTo, lessThan) */
isPgConnectionFilterOperator?: boolean;
/** True if this field is a logical operator (and/or/not) */
isPgConnectionFilterOperatorLogical?: boolean;
/** True if this is a many-relation filter field */
isPgConnectionFilterManyField?: boolean;
}

interface BehaviorStrings {
filter: true;
filterProc: true;
'attribute:filterBy': true;
}

interface SchemaOptions {
connectionFilterArrays?: boolean;
connectionFilterLogicalOperators?: boolean;
connectionFilterAllowNullInput?: boolean;
connectionFilterAllowedFieldTypes?: string[];
connectionFilterAllowedOperators?: string[];
connectionFilterOperatorNames?: Record<string, string>;
connectionFilterSetofFunctions?: boolean;
connectionFilterComputedColumns?: boolean;
connectionFilterRelations?: boolean;
/** If true (default), relation filter fields are only added for FKs with supporting indexes.
* This prevents generating EXISTS subqueries that would cause sequential scans on large tables.
* Set to false to allow relation filters on all FKs regardless of index status. */
connectionFilterRelationsRequireIndex?: boolean;
}
}

namespace GraphileConfig {
interface Plugins {
ConnectionFilterInflectionPlugin: true;
ConnectionFilterTypesPlugin: true;
ConnectionFilterArgPlugin: true;
ConnectionFilterAttributesPlugin: true;
ConnectionFilterOperatorsPlugin: true;
ConnectionFilterCustomOperatorsPlugin: true;
ConnectionFilterLogicalOperatorsPlugin: true;
ConnectionFilterComputedAttributesPlugin: true;
ConnectionFilterForwardRelationsPlugin: true;
ConnectionFilterBackwardRelationsPlugin: true;
}
}
}
74 changes: 74 additions & 0 deletions graphile/graphile-connection-filter/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* graphile-connection-filter
*
* A PostGraphile v5 native connection filter plugin.
* Adds advanced filtering capabilities to connection and list fields.
*
* @example
* ```typescript
* import { ConnectionFilterPreset } from 'graphile-connection-filter';
*
* const preset = {
* extends: [
* ConnectionFilterPreset(),
* ],
* };
* ```
*
* For satellite plugins that need to register custom operators:
* ```typescript
* // In your plugin's init hook:
* if (typeof build.addConnectionFilterOperator === 'function') {
* build.addConnectionFilterOperator('MyType', 'myOperator', {
* description: 'My custom operator',
* resolve: (sqlIdentifier, sqlValue) => sql`${sqlIdentifier} OP ${sqlValue}`,
* });
* }
* ```
*
* For satellite plugins that need to access the query builder from a filter apply:
* ```typescript
* import { getQueryBuilder } from 'graphile-connection-filter';
* // In your filter field's apply callback:
* const qb = getQueryBuilder(build, $condition);
* if (qb) {
* const idx = qb.selectAndReturnIndex(sql`...`);
* qb.setMeta('key', { selectIndex: idx });
* }
* ```
*/

export { ConnectionFilterPreset } from './preset';

// Re-export all plugins for granular use
export {
ConnectionFilterInflectionPlugin,
ConnectionFilterTypesPlugin,
ConnectionFilterArgPlugin,
ConnectionFilterAttributesPlugin,
ConnectionFilterOperatorsPlugin,
ConnectionFilterCustomOperatorsPlugin,
ConnectionFilterLogicalOperatorsPlugin,
ConnectionFilterComputedAttributesPlugin,
ConnectionFilterForwardRelationsPlugin,
ConnectionFilterBackwardRelationsPlugin,
makeApplyFromOperatorSpec,
} from './plugins';

// Re-export types
export type {
ConnectionFilterOperatorSpec,
ConnectionFilterOptions,
ConnectionFilterOperatorsDigest,
PgConnectionFilterOperatorsScope,
} from './types';
export { $$filters } from './types';

// Re-export utilities
export {
isEmpty,
makeAssertAllowed,
getQueryBuilder,
isComputedScalarAttributeResource,
getComputedAttributeResources,
} from './utils';
Loading
Loading