Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support passing arguments to functions as filters #213

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
26 changes: 26 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,32 @@ query {
}
```

#### Computed Columns with Arguments

```graphql
query {
allPeople(
filter: {
computedColumn: {
equalTo: 17
args: { firstArgument: 1, secondArgument: 2 }
}
}
) {
nodes {
firstName
lastName
}
}
}
```

The `args` are passed to the SQL function that is resposible for creating the computed column:

```sql
FUNCTION people_computed_column(person people, first_argument int, second_argument int)
```

#### Relations: Nested

```graphql
Expand Down
102 changes: 75 additions & 27 deletions src/PgConnectionArgFilterComputedColumnsPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Plugin } from "graphile-build";
import type { PgClass, PgProc, PgType } from "graphile-build-pg";
import { ConnectionFilterResolver } from "./PgConnectionArgFilterPlugin";
import camelCase from "camelcase";

const PgConnectionArgFilterComputedColumnsPlugin: Plugin = (
builder,
Expand Down Expand Up @@ -31,6 +32,9 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = (

connectionFilterTypesByTypeName[Self.name] = Self;

let computedColumnNames: string[] = [];
let argumentLists: { name: string; type: PgType }[][] = [];

const procByFieldName = (
introspectionResultsByKind.procedure as PgProc[]
).reduce((memo: { [fieldName: string]: PgProc }, proc) => {
Expand All @@ -49,19 +53,6 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = (
proc
);
if (!computedColumnDetails) return memo;
const { pseudoColumnName } = computedColumnDetails;

// Must have only one required argument
const inputArgsCount = proc.argTypeIds.filter(
(_typeId, idx) =>
proc.argModes.length === 0 || // all args are `in`
proc.argModes[idx] === "i" || // this arg is `in`
proc.argModes[idx] === "b" // this arg is `inout`
).length;
const nonOptionalArgumentsCount = inputArgsCount - proc.argDefaultsNum;
if (nonOptionalArgumentsCount > 1) {
return memo;
}

// Must return a scalar or an array
if (proc.returnsSet) return memo;
Expand All @@ -75,39 +66,76 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = (
if (isVoid) return memo;

// Looks good
const { argNames, argTypes, pseudoColumnName } = computedColumnDetails;
const fieldName = inflection.computedColumn(
pseudoColumnName,
proc,
table
);

const args: { name: string; type: PgType }[] = [];
// The first argument is of table type. It is not exposed to the schema.
for (let i = 1; i < argNames.length; i++) {
args.push({
name: camelCase(argNames[i]),
type: argTypes[i],
});
}

computedColumnNames.push(pseudoColumnName);
argumentLists.push(args);

memo = build.extend(memo, { [fieldName]: proc });
return memo;
}, {});

const operatorsTypeNameByFieldName: { [fieldName: string]: string } = {};

const procFields = Object.entries(procByFieldName).reduce(
(memo, [fieldName, proc]) => {
(memo, [fieldName, proc], index) => {
const hasArgsField: boolean = argumentLists[index].length >= 1;

const computedColumnWithArgsDetails = hasArgsField
? {
name: computedColumnNames[index],
arguments: argumentLists[index],
}
: undefined;

const OperatorsType = connectionFilterOperatorsType(
newWithHooks,
proc.returnTypeId,
null
null,
computedColumnWithArgsDetails
);
if (!OperatorsType) {
return memo;
}
operatorsTypeNameByFieldName[fieldName] = OperatorsType.name;

const createdField = fieldWithHooks(
fieldName,
{
description: `Filter by the object’s \`${fieldName}\` field.`,
type: OperatorsType,
},
{
isPgConnectionFilterField: true,
}
);

if (hasArgsField) {
// The args field resolver doesn't do anything. The args are
// handled in the resolver of the computed column (below).
connectionFilterRegisterResolver(
createdField.type.name,
"args",
() => null
);
}

return extend(memo, {
[fieldName]: fieldWithHooks(
fieldName,
{
description: `Filter by the object’s \`${fieldName}\` field.`,
type: OperatorsType,
},
{
isPgConnectionFilterField: true,
}
),
[fieldName]: createdField,
});
},
{}
Expand All @@ -121,10 +149,29 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = (
}) => {
if (fieldValue == null) return null;

const queryParameters: { [key: string]: any } = fieldValue;
const providedArgs = queryParameters["args"];

const proc = procByFieldName[fieldName];

// Collect arguments of the computed column and add it
// to the sql function arguments.
let sqlFunctionArguments = [sql.fragment`${sourceAlias}`];
// The first function argument (table type) is already set above.
for (let i = 1; i < proc.argNames.length; i++) {
const nameOfArgument = camelCase(proc.argNames[i]);
const providedArgValue = providedArgs?.[nameOfArgument];
if (providedArgValue === undefined)
throw new Error(
`The value for argument ${nameOfArgument} is missing.`
);

sqlFunctionArguments.push(sql.fragment`${sql.value(providedArgValue)}`);
}

const sqlIdentifier = sql.query`${sql.identifier(
proc.namespace.name
)}.${sql.identifier(proc.name)}(${sourceAlias})`;
)}.${sql.identifier(proc.name)}(${sql.join(sqlFunctionArguments, ",")})`;
const pgType = introspectionResultsByKind.typeById[proc.returnTypeId];
const pgTypeModifier = null;
const filterTypeName = operatorsTypeNameByFieldName[fieldName];
Expand Down Expand Up @@ -175,8 +222,9 @@ const PgConnectionArgFilterComputedColumnsPlugin: Plugin = (
return null;
}

const argNames = proc.argNames;
const pseudoColumnName = proc.name.substr(table.name.length + 1);
return { argTypes, pseudoColumnName };
return { argNames, argTypes, pseudoColumnName };
}
};

Expand Down
73 changes: 64 additions & 9 deletions src/PgConnectionArgFilterPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,12 @@ const PgConnectionArgFilterPlugin: Plugin = (
builder.hook("build", (build) => {
const {
extend,
graphql: { getNamedType, GraphQLInputObjectType, GraphQLList },
graphql: {
getNamedType,
GraphQLInputObjectType,
GraphQLList,
GraphQLNonNull,
},
inflection,
pgIntrospectionResultsByKind: introspectionResultsByKind,
pgGetGqlInputTypeByTypeIdAndModifier,
Expand Down Expand Up @@ -230,7 +235,14 @@ const PgConnectionArgFilterPlugin: Plugin = (
const connectionFilterOperatorsType = (
newWithHooks: any,
pgTypeId: number,
pgTypeModifier: number
pgTypeModifier: number,
computedColumnWithArgsDetails?: {
name: string;
arguments: {
name: string;
type: PgType;
}[];
}
) => {
const pgType = introspectionResultsByKind.typeById[pgTypeId];

Expand Down Expand Up @@ -349,7 +361,10 @@ const PgConnectionArgFilterPlugin: Plugin = (
: null;

const isListType = fieldType instanceof GraphQLList;
const operatorsTypeName = isListType

const operatorsTypeName = computedColumnWithArgsDetails
? `ComputedColumnWithArgs_${computedColumnWithArgsDetails.name}`
: isListType
? inflection.filterFieldListType(namedType.name)
: inflection.filterFieldType(namedType.name);

Expand All @@ -367,14 +382,54 @@ const PgConnectionArgFilterPlugin: Plugin = (
// fully defined with fields, so return it
return existingType;
}

let connectionFilterOperatorsTypeConfig: {
name: string;
description?: string;
fields?: {
args: {
type: any;
};
};
} = {
name: operatorsTypeName,
description: `A filter to be used against ${namedType.name}${
isListType ? " List" : ""
} fields. All fields are combined with a logical ‘and.’`,
};

if (computedColumnWithArgsDetails) {
// Create a type for the args field
let argFields: { [key: string]: any } = {};
computedColumnWithArgsDetails.arguments.forEach((argument) => {
argFields[argument.name] = {
type: new GraphQLNonNull(
pgGetGqlInputTypeByTypeIdAndModifier(argument.type.id)
),
};
});

const argsType = newWithHooks(
GraphQLInputObjectType,
{
name: `${operatorsTypeName}_Arguments`,
fields: argFields,
},
{},
true
);

// Add the args field to the filter-operator type
connectionFilterOperatorsTypeConfig.fields = {
args: {
type: new GraphQLNonNull(argsType),
},
};
}

return newWithHooks(
GraphQLInputObjectType,
{
name: operatorsTypeName,
description: `A filter to be used against ${namedType.name}${
isListType ? " List" : ""
} fields. All fields are combined with a logical ‘and.’`,
},
connectionFilterOperatorsTypeConfig,
{
isPgConnectionFilterOperators: true,
pgConnectionFilterOperatorsCategory,
Expand Down