Skip to content

Commit

Permalink
feat: Reverse filter (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicola-smartive authored Nov 23, 2023
1 parent 3ffc9a5 commit 06c2568
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 40 deletions.
1 change: 1 addition & 0 deletions src/models/model-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type EntityFieldDefinition = FieldDefinitionBase &
| {
default?: Value;
};
reverseFilterable?: boolean;
searchable?: boolean;
orderable?: boolean;
comparable?: boolean;
Expand Down
49 changes: 40 additions & 9 deletions src/resolvers/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import { EntityModel, FullContext } from '..';
import { ForbiddenError, UserInputError } from '../errors';
import { OrderBy, Where, normalizeArguments } from './arguments';
import { FieldResolverNode } from './node';
import { Joins, Ops, addJoin, apply, getColumn, ors } from './utils';
import { Joins, QueryBuilderOps, addJoin, apply, applyJoins, getColumn, ors } from './utils';

export const SPECIAL_FILTERS: Record<string, string> = {
GT: '?? > ?',
GTE: '?? >= ?',
LT: '?? < ?',
LTE: '?? <= ?',
SOME: 'SOME',
};

export type WhereNode = {
ctx: FullContext;

rootModel: EntityModel;
rootTableAlias: string;

model: EntityModel;
Expand Down Expand Up @@ -69,7 +69,7 @@ export const applyFilters = (node: FieldResolverNode, query: Knex.QueryBuilder,
}

if (where) {
const ops: Ops<Knex.QueryBuilder> = [];
const ops: QueryBuilderOps = [];
applyWhere(node, where, ops, joins);
void apply(query, ops);
}
Expand All @@ -79,13 +79,48 @@ export const applyFilters = (node: FieldResolverNode, query: Knex.QueryBuilder,
}
};

const applyWhere = (node: WhereNode, where: Where, ops: Ops<Knex.QueryBuilder>, joins: Joins) => {
const applyWhere = (node: WhereNode, where: Where, ops: QueryBuilderOps, joins: Joins) => {
const aliases = node.ctx.aliases;

for (const key of Object.keys(where)) {
const value = where[key];

const specialFilter = key.match(/^(\w+)_(\w+)$/);
if (specialFilter) {
const [, actualKey, filter] = specialFilter;

if (filter === 'SOME') {
const reverseRelation = node.model.getReverseRelation(actualKey);
const rootTableAlias = `${node.tableAlias}__W__${key}`;
const targetModel = reverseRelation.targetModel;
const tableAlias = targetModel === targetModel.rootModel ? rootTableAlias : `${node.tableAlias}__WS_${key}`;

const subWhereNode: WhereNode = {
ctx: node.ctx,
rootTableAlias,
model: targetModel,
tableAlias,
};
const subOps: QueryBuilderOps = [];
const subJoins: Joins = [];
applyWhere(subWhereNode, value as Where, subOps, subJoins);

// TODO: make this work with subtypes
ops.push((query) =>
query.whereExists((subQuery) => {
void subQuery
.from(`${targetModel.name} as ${aliases.getShort(tableAlias)}`)
.whereRaw(`?? = ??`, [
`${aliases.getShort(tableAlias)}.${reverseRelation.field.foreignKey}`,
`${aliases.getShort(node.tableAlias)}.id`,
]);
void apply(subQuery, subOps);
applyJoins(aliases, subQuery, subJoins);
})
);
continue;
}

if (!SPECIAL_FILTERS[filter]) {
// Should not happen
throw new Error(`Invalid filter ${key}.`);
Expand All @@ -99,15 +134,11 @@ const applyWhere = (node: WhereNode, where: Where, ops: Ops<Knex.QueryBuilder>,
if (field.kind === 'relation') {
const relation = node.model.getRelation(field.name);
const targetModel = relation.targetModel;
const rootModel = targetModel.parentModel || targetModel;
const rootTableAlias = `${node.model.name}__W__${key}`;
const tableAlias = targetModel === rootModel ? rootTableAlias : `${node.model.name}__WS__${key}`;
const tableAlias = targetModel === targetModel.rootModel ? rootTableAlias : `${node.model.name}__WS__${key}`;
const subNode: WhereNode = {
ctx: node.ctx,

rootModel,
rootTableAlias,

model: targetModel,
tableAlias,
};
Expand Down
2 changes: 2 additions & 0 deletions src/resolvers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export const getNameOrAlias = (node: FieldNode) => {

export type Ops<T> = ((target: T) => T)[];

export type QueryBuilderOps = Ops<Knex.QueryBuilder>;

export const apply = <T>(target: T, ops: ((target: T) => T)[]) => ops.reduce((target, op) => op(target), target);

type Join = { table1Alias: string; column1: string; table2Name: string; table2Alias: string; column2: string };
Expand Down
17 changes: 11 additions & 6 deletions src/schema/generate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DefinitionNode, DocumentNode, GraphQLSchema, buildASTSchema, print } from 'graphql';
import { Models } from '../models/models';
import { isQueriableField, isRelation, isRootModel, typeToField } from '../models/utils';
import { isQueriableField, isRootModel, typeToField } from '../models/utils';
import { Field, document, enm, iface, input, object, scalar } from './utils';

export const generateDefinitions = ({
Expand Down Expand Up @@ -77,12 +77,17 @@ export const generateDefinitions = ({
{ name: `${field.name}_LT`, type: field.type },
{ name: `${field.name}_LTE`, type: field.type },
]),
...model.fields
.filter(isRelation)
.filter(({ filterable }) => filterable)
.map(({ name, type }) => ({
...model.relations
.filter(({ field: { filterable } }) => filterable)
.map(({ name, targetModel }) => ({
name,
type: `${type}Where`,
type: `${targetModel.name}Where`,
})),
...model.reverseRelations
.filter(({ field: { reverseFilterable } }) => reverseFilterable)
.map((relation) => ({
name: `${relation.name}_SOME`,
type: `${relation.targetModel.name}Where`,
})),
]),
input(
Expand Down
74 changes: 74 additions & 0 deletions tests/api/__snapshots__/query.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
exports[`query can be executed 1`] = `
{
"manyObjects": [
{
"another": {
"id": "226a20e8-5c18-4423-99ca-eb0df6ff4fdd",
"manyObjects": [
{
"field": null,
"id": "604ab55d-ec3e-4857-9f27-219158f80e64",
},
],
},
"field": null,
"id": "fc4e013e-4cb0-4ef8-9f2e-3d475bdf2b90",
"xyz": 2,
},
{
"another": {
"id": "226a20e8-5c18-4423-99ca-eb0df6ff4fdd",
Expand All @@ -20,3 +34,63 @@ exports[`query can be executed 1`] = `
],
}
`;

exports[`query processes reverseFilters correctly 1`] = `
{
"all": [
{
"id": "226a20e8-5c18-4423-99ca-eb0df6ff4fdd",
"manyObjects": [
{
"float": 0.5,
},
{
"float": 0,
},
],
},
{
"id": "ba5d94a8-0035-4e45-9258-2f7676eb8d18",
"manyObjects": [
{
"float": 0.5,
},
],
},
],
"withFloat0": [
{
"id": "226a20e8-5c18-4423-99ca-eb0df6ff4fdd",
"manyObjects": [
{
"float": 0.5,
},
{
"float": 0,
},
],
},
],
"withFloat0_5": [
{
"id": "226a20e8-5c18-4423-99ca-eb0df6ff4fdd",
"manyObjects": [
{
"float": 0.5,
},
{
"float": 0,
},
],
},
{
"id": "ba5d94a8-0035-4e45-9258-2f7676eb8d18",
"manyObjects": [
{
"float": 0.5,
},
],
},
],
}
`;
29 changes: 29 additions & 0 deletions tests/api/query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,33 @@ describe('query', () => {
).toMatchSnapshot();
});
});

it('processes reverseFilters correctly', async () => {
await withServer(async (request) => {
expect(
await request(gql`
query ReverseFiltersQuery {
all: anotherObjects {
id
manyObjects {
float
}
}
withFloat0: anotherObjects(where: { manyObjects_SOME: { float: 0 } }) {
id
manyObjects {
float
}
}
withFloat0_5: anotherObjects(where: { manyObjects_SOME: { float: 0.5 } }) {
id
manyObjects {
float
}
}
}
`)
).toMatchSnapshot();
});
});
});
2 changes: 2 additions & 0 deletions tests/generated/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type AnotherObjectOrderBy = {
export type AnotherObjectWhere = {
deleted?: InputMaybe<Array<Scalars['Boolean']['input']>>;
id?: InputMaybe<Array<Scalars['ID']['input']>>;
manyObjects_SOME?: InputMaybe<SomeObjectWhere>;
};

export type AnotherObjectWhereUnique = {
Expand Down Expand Up @@ -612,6 +613,7 @@ export type SomeObjectOrderBy = {
export type SomeObjectWhere = {
another?: InputMaybe<AnotherObjectWhere>;
deleted?: InputMaybe<Array<Scalars['Boolean']['input']>>;
float?: InputMaybe<Array<Scalars['Float']['input']>>;
id?: InputMaybe<Array<Scalars['ID']['input']>>;
};

Expand Down
40 changes: 16 additions & 24 deletions tests/generated/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export type AnotherObjectOrderBy = {
export type AnotherObjectWhere = {
deleted?: InputMaybe<Array<Scalars['Boolean']['input']>>;
id?: InputMaybe<Array<Scalars['ID']['input']>>;
manyObjects_SOME?: InputMaybe<SomeObjectWhere>;
};

export type AnotherObjectWhereUnique = {
Expand Down Expand Up @@ -609,6 +610,7 @@ export type SomeObjectOrderBy = {
export type SomeObjectWhere = {
another?: InputMaybe<AnotherObjectWhere>;
deleted?: InputMaybe<Array<Scalars['Boolean']['input']>>;
float?: InputMaybe<Array<Scalars['Float']['input']>>;
id?: InputMaybe<Array<Scalars['ID']['input']>>;
};

Expand Down Expand Up @@ -851,6 +853,11 @@ export type SomeQueryQueryVariables = Exact<{ [key: string]: never; }>;

export type SomeQueryQuery = { manyObjects: Array<{ __typename: 'SomeObject', id: string, field: string | null, xyz: number, another: { __typename: 'AnotherObject', id: string, manyObjects: Array<{ __typename: 'SomeObject', id: string, field: string | null }> } }> };

export type ReverseFiltersQueryQueryVariables = Exact<{ [key: string]: never; }>;


export type ReverseFiltersQueryQuery = { first: Array<{ __typename: 'AnotherObject', id: string, manyObjects: Array<{ __typename: 'SomeObject', float: number }> }>, second: Array<{ __typename: 'AnotherObject', id: string, manyObjects: Array<{ __typename: 'SomeObject', float: number }> }> };

export type DeleteAnotherObjectMutationMutationVariables = Exact<{
id: Scalars['ID']['input'];
}>;
Expand Down Expand Up @@ -981,16 +988,6 @@ export type RestoreAnswerMutationMutationVariables = Exact<{

export type RestoreAnswerMutationMutation = { restoreAnswer: string };

export type AnotherQueryQueryVariables = Exact<{ [key: string]: never; }>;


export type AnotherQueryQuery = { manyObjects: Array<{ __typename: 'SomeObject', id: string, field: string | null, another: { __typename: 'AnotherObject', id: string, manyObjects: Array<{ __typename: 'SomeObject', id: string, field: string | null }> } }> };

export type YetAnotherQueryQueryVariables = Exact<{ [key: string]: never; }>;


export type YetAnotherQueryQuery = { someObject: { __typename: 'SomeObject', id: string, field: string | null } };

type DiscriminateUnion<T, U> = T extends U ? T : never;

export namespace DeleteAnotherObject {
Expand Down Expand Up @@ -1050,6 +1047,15 @@ export namespace SomeQuery {
export type _manyObjects = NonNullable<(NonNullable<(NonNullable<NonNullable<(NonNullable<SomeQueryQuery['manyObjects']>)[number]>['another']>)['manyObjects']>)[number]>;
}

export namespace ReverseFiltersQuery {
export type Variables = ReverseFiltersQueryQueryVariables;
export type query = ReverseFiltersQueryQuery;
export type first = NonNullable<(NonNullable<ReverseFiltersQueryQuery['first']>)[number]>;
export type manyObjects = NonNullable<(NonNullable<NonNullable<(NonNullable<ReverseFiltersQueryQuery['first']>)[number]>['manyObjects']>)[number]>;
export type second = NonNullable<(NonNullable<ReverseFiltersQueryQuery['second']>)[number]>;
export type _manyObjects = NonNullable<(NonNullable<NonNullable<(NonNullable<ReverseFiltersQueryQuery['second']>)[number]>['manyObjects']>)[number]>;
}

export namespace DeleteAnotherObjectMutation {
export type Variables = DeleteAnotherObjectMutationMutationVariables;
export type mutation = DeleteAnotherObjectMutationMutation;
Expand Down Expand Up @@ -1147,17 +1153,3 @@ export namespace RestoreAnswerMutation {
export type Variables = RestoreAnswerMutationMutationVariables;
export type mutation = RestoreAnswerMutationMutation;
}

export namespace AnotherQuery {
export type Variables = AnotherQueryQueryVariables;
export type query = AnotherQueryQuery;
export type manyObjects = NonNullable<(NonNullable<AnotherQueryQuery['manyObjects']>)[number]>;
export type another = (NonNullable<NonNullable<(NonNullable<AnotherQueryQuery['manyObjects']>)[number]>['another']>);
export type _manyObjects = NonNullable<(NonNullable<(NonNullable<NonNullable<(NonNullable<AnotherQueryQuery['manyObjects']>)[number]>['another']>)['manyObjects']>)[number]>;
}

export namespace YetAnotherQuery {
export type Variables = YetAnotherQueryQueryVariables;
export type query = YetAnotherQueryQuery;
export type someObject = (NonNullable<YetAnotherQueryQuery['someObject']>);
}
4 changes: 3 additions & 1 deletion tests/generated/models.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"kind": "relation",
"type": "AnotherObject",
"filterable": true,
"reverseFilterable": true,
"updatable": true,
"nonNull": true,
"foreignKey": "anotherId"
Expand All @@ -141,7 +142,8 @@
"type": "Float",
"scale": 1,
"precision": 1,
"nonNull": true
"nonNull": true,
"filterable": true
},
{
"kind": "enum",
Expand Down
2 changes: 2 additions & 0 deletions tests/generated/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ input AnotherObjectOrderBy {
input AnotherObjectWhere {
id: [ID!]
deleted: [Boolean!]
manyObjects_SOME: SomeObjectWhere
}

input AnotherObjectWhereUnique {
Expand Down Expand Up @@ -262,6 +263,7 @@ input SomeObjectOrderBy {

input SomeObjectWhere {
id: [ID!]
float: [Float!]
deleted: [Boolean!]
another: AnotherObjectWhere
}
Expand Down
Loading

0 comments on commit 06c2568

Please sign in to comment.