Skip to content
Open
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
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"
}
}
109 changes: 109 additions & 0 deletions graphile/graphile-connection-filter/src/augmentations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* 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;
/** 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;
connectionFilterAllowEmptyObjectInput?: boolean;
connectionFilterAllowedFieldTypes?: string[];
connectionFilterAllowedOperators?: string[];
connectionFilterOperatorNames?: Record<string, string>;
connectionFilterSetofFunctions?: boolean;
connectionFilterComputedColumns?: boolean;
connectionFilterRelations?: 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;
}
}
}
63 changes: 63 additions & 0 deletions graphile/graphile-connection-filter/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* 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:
* const addConnectionFilterOperator = (build as any).addConnectionFilterOperator;
* if (typeof addConnectionFilterOperator === 'function') {
* addConnectionFilterOperator('MyType', 'myOperator', {
* description: 'My custom operator',
* resolve: (sqlIdentifier, sqlValue) => sql`${sqlIdentifier} OP ${sqlValue}`,
* });
* }
* ```
*/

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,
isComputedScalarAttributeResource,
getComputedAttributeResources,
} from './utils';
Loading
Loading