graphqlpostgraphile

Postgraphil Graphql - how to filter query only if the input is not null and not empty


Im using postgraphile and i have this query:

query Products($categories: [String]){
  products(
    filter: {
       category: { in: $categories } 
    }) {
    id
    name
    category
  }
}

is there a way to not use the filter if the $categories array is empty ?

In case the array is not set or it's empty, i want to get all the results.

i saw there is an option to pass filter as an argument, but I wonder if there is another support.


Solution

  • Without making the filter a generic argument on the client side, There are two server-side options for filter operator customization in the postgraphile-plugin-connection-filter (v2.3.0) plugin. Both require you to create an additional plugin that registers a build hook.

    Option 1: Use addConnectionFilterOperator

    addConnectionFilterOperator is a build extension function added by ConnectionFilterPlugin that is meant to allow you to add a new operators for custom functionality.

    Option 2: Modify Connection Filter Operators Directly

    It's also possible to directly change the SQL output of a given operator by modifying the OperatorSpec values for each GraphQLType. These specs can be found in the following fields on the Build object:

    connectionFilterArrayOperators,
    connectionFilterEnumOperators,
    connectionFilterRangeOperators,
    connectionFilterScalarOperators
    

    Below is an example of a combined Plugin implementation for Options 1 and 2 that omit SQL generation when the argument value array for the operator is empty - effectively nullifying the operator and resulting in full data returns. This implementation allows for easy switching between modifying the in operators directly and adding new expansiveIn operators to each connection filter's scalar fields.

    import type { Build } from "postgraphile";
    import type { Plugin, Options } from "graphile-build";
    import type { AddConnectionFilterOperator } from "postgraphile-plugin-connection-filter/dist/PgConnectionArgFilterPlugin";
    import type { PgType, SQL } from "graphile-build-pg";
    import type { GraphQLInputType } from "graphql";
    import type { OperatorSpec } from "postgraphile-plugin-connection-filter/dist/PgConnectionArgFilterOperatorsPlugin";
    
    export interface InOperatorEmptyArrayCustomizationPluginBuildOpts {
      inOperatorEmptyArrayCustomizationPlugin?: {
        /**
         * Add new "expansiveIn" operators with custom empty array instead of
         * modifying existing "in" operators
         */
        addNewOperator?: boolean;
      };
    }
    
    /**
     * Implements custom empty array handling either by registering a new "expansiveIn"
     * operator for each connection filter or by modifying the existing "operators".
     * This plugin must be appended AFTER postgraphile-plugin-connection-filter
     * as it depends on its build extensions.
     */
    const InOperatorEmptyArrayCustomizationPlugin: Plugin = (builder, options) => {
      const { inOperatorEmptyArrayCustomizationPlugin } = options as Options &
        InOperatorEmptyArrayCustomizationPluginBuildOpts;
    
      const addNewOperator =
        inOperatorEmptyArrayCustomizationPlugin?.addNewOperator === true;
    
      // create a build hook to access ConnectionFilterPlugin build extensions.
      builder.hook("build", (build) => {
        const {
          pgSql: sql,
          graphql: { GraphQLList, GraphQLNonNull },
    
          // this function is added as a build extension by the ConnectionFilterPlugin
          // and allows for the addition of custom filter operators.
          addConnectionFilterOperator,
          gql2pg,
    
          // this contains all existing ConnectionFilterPlugin scalar operators
          // by GraphQL type
          connectionFilterScalarOperators,
        } = build as Build & {
          addConnectionFilterOperator: AddConnectionFilterOperator;
          connectionFilterScalarOperators: Record<
            string,
            Record<string, OperatorSpec>
          >;
        };
    
        // Generate "in" SQL fragment from argument input values if values are
        // present in the array. Otherwise, return null.
        const resolveListSqlValue = (
          input: unknown,
          pgType: PgType,
          pgTypeModifier: number | null,
          resolveListItemSqlValue: (
            elem: unknown,
            pgType: PgType,
            pgTypeModifier: number | null
          ) => unknown
        ) =>
          (input as unknown[]).length === 0
            ? null
            : sql.query`(${sql.join(
                (input as unknown[]).map((i) =>
                  resolveListItemSqlValue
                    ? resolveListItemSqlValue(i, pgType, pgTypeModifier)
                    : gql2pg(i, pgType, pgTypeModifier)
                ),
                ","
              )})`;
    
        // checks whether value is present before adding the sql filter fragment.
        const resolve = (i: SQL, v: SQL) =>
          v != null ? sql.fragment`${i} IN ${v}` : null;
    
        // Find all the scalar GraphQLTypes that have an "in" operator.
        const typesWithScalarInOperators = Object.entries(
          connectionFilterScalarOperators
        )
          .filter(([, operations]) => operations.in)
          .map(([typeName]) => typeName);
    
        // modify existing "in" operators for every scalar type.
        if (!addNewOperator) {
          // The graphile build engine will emit a warning if you create
          // a new build object using the standard javascript mechanisms.
          // It will also throw an error if the existing
          // connectionFilterScalarOperations field is replaced in the extension
          // object...
          const extendedBuild = build.extend(build, {});
    
          // ...so we merge in the new operators in a separate step.
          typesWithScalarInOperators.forEach((typeName) => {
            extendedBuild.connectionFilterScalarOperators[typeName].in = {
              // see https://github.com/graphile-contrib/postgraphile-plugin-connection-filter/blob/v2.3.0/src/PgConnectionArgFilterOperatorsPlugin.ts#L80-L85
              // for existing "in" operator configuration
              ...extendedBuild.connectionFilterScalarOperators[typeName].in,
              resolveSqlValue: resolveListSqlValue,
              resolve,
            };
          });
    
          return extendedBuild;
        }
    
        // Otherwise add a new operator called "inExpansive" that implements the custom
        // empty array argument handling.
        // see https://github.com/graphile-contrib/postgraphile-plugin-connection-filter/blob/v2.3.0/__tests__/customOperatorsPlugin.ts
        // for `addConnectionFilterOperator` usage examples.
        addConnectionFilterOperator(
          // add the new operator to any type that has an "in" operator.
          typesWithScalarInOperators,
          "inExpansive",
          "Included in the specified list -unless list is empty in which case this operator is not applied.",
          // list of non-null element type
          (fieldInputType: GraphQLInputType) =>
            new GraphQLList(new GraphQLNonNull(fieldInputType)),
          resolve,
          {
            resolveSqlValue: resolveListSqlValue,
          }
        );
    
        return build;
      });
    };
    
    export default InOperatorEmptyArrayCustomizationPlugin;
    

    Append plugin after ConnectionFilterPlugin in the Postgraphile middleware options:

    // ...
    appendPlugins: [
      // ..
      ConnectionFilterPlugin,
      AddInExpansiveFilterOperatorPlugin
    ],
    // ...
    

    To enable the expansiveIn operator (Option 1) add the relevant configuration to `graphileBuildOptions in the Postgraphile middleware options:

    graphileBuildOptions: {
      inOperatorEmptyArrayCustomizationPlugin: {
       addNewOperator: true
      },
      // other plugin options
    }
    

    You can use inExpansive operator the same way as the in operator:

    query Products($categories: [String]){
      products(
        filter: {
           category: { inExpansive: $categories } 
        }) {
        id
        name
        category
      }
    }