Skip to content

Commit

Permalink
update filtering logic to support object id
Browse files Browse the repository at this point in the history
  • Loading branch information
eric-burel committed Jun 22, 2022
1 parent 059004e commit be29b8b
Show file tree
Hide file tree
Showing 13 changed files with 489 additions and 39 deletions.
240 changes: 240 additions & 0 deletions docusaurus/docs/vulcan-fire/filtering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
---
title: Filtering
---

When loading data, you will usually want to apply some kind of filtering or sorting to your query. Vulcan offers the following tools to help you get the data you need, whether you need to select a single document or a range of items.

#### Database vs GraphQL Fields

Note that all sorting, filtering, etc. operations happen at the database level. So in all following examples, you can only filter and sort by **database** fields, not GraphQL-only fields.

## Query Arguments

The following arguments are available for both single-document and multi-documents queries.

### Filter

Vulcan queries and mutations take a `filter` argument to help you target one or more specific documents.

The `filter` argument can either accept a list of fields; or special `_and`, `_not`, and `_or` operators used to combine multiple `filter` selectors.

Each field can then in turn receive an operator such as `_eq` or `_in` depending on its type. Note that this API was heavily inspired by the [Hasura](https://hasura.io/docs/latest/graphql/core/databases/postgres/queries/query-filters/) API.

Here is an example `MovieFilterInput` input type:

```gql
input MovieFilterInput {
_and: [MovieFilterInput]
_not: MovieFilterInput
_or: [MovieFilterInput]
_id: String_Selector
createdAt: Date_Selector
userId: String_Selector
slug: String_Selector
title: String_Selector
}
```

And here is an example query using `filter`:

```gql
query MyMovie {
movie(input: { filter: { _id: { _eq: "123foo" } } }) {
result{
_id
title
year
}
}
}
```

You can use the `filter` argument to query for single documents, but if the filter matches more than one document only the first one will be returned.

#### With Mongo

Available filters are visible in your GraphQL schema. You can search for any type ending in `_Selector`: `String_Selector`, `Float_Selector`...

Most parameters have transparent meaning: `_eq`, `_lt`, `_lte`, `_gt`, `_gte`, `_in` (for an array of values) etc.

The `_like` filter for strings will be translated as a case insensitive [`$regex`](https://www.mongodb.com/docs/manual/reference/operator/query/regex/) call.

```ts
// Conversion table from Vulcan filters to Mongo selectors
_is_null: (value) => ({ $exists: !value }),
_is: (value) => ({ $elemMatch: { $eq: value } }),
_contains: (value) => ({ $elemMatch: { $eq: value } }),
_like: (value) => ({
$regex: value,
$options: "i",
}),
```

If you use `ObjectId` for document ids instead of strings, the `GraphqlObjectId_Selector` will be used.
It supports basic operations: `_eq`, `_in`, `_is_null`, `_neq`.

#### With other databases

The `_filter` function of each database connector translates Vulcan filters into a format that your database can understand.
We only support Mongo out-of-the-box, but you can use it as an example to support more databases, SQL or No-SQL.

#### Custom Filters

There are also cases where your query is too complex to be able to define it through the GraphQL API. For example, you could imagine a scenario where you have `Movies` and `Reviews` rating them on a five-star scale, and want to filter movies to only show those with a specific rating average.

In this case, you would define a server-side `_withRating` filter (starting with an underscore is a convention to differentiate the filter from field names), and then reference it in your query:

```
query MoviesWithRating {
movies(input: { filter: { _withRating: { average: 4 } } }) {
results{
_id
title
year
description
}
}
}
```

The filter function takes an argument object with the query `input` and `context` as properties; and should return an object with `selector` and `options` properties. Here's how you would define it:

```ts
const Movies = createGraphqlModelServer({
name: 'Movies',

graphql: {
typeName: 'Movie',
schema,
}

crud: {
customFilters: [
{
name: '_withRating',
arguments: 'average: Int',
filter: ({ input, context, filterArguments }) => {
const { average } = filterArguments;
const { Reviews } = context;
// get all movies that have an average review score of X stars
const xStarReviewsMoviesIds = getMoviesByScore(average);
return {
selector: { _id: {$in: xStarReviewsMoviesIds } },
options: {}
}
};
}
]
},
});
```

##### Custom Filters & Nested Fields

Custom filters can be useful to work around the limitations of the filtering system. For example, unlike MongoDB the GraphQL filtering API does not let you filter based on nested document fields (e.g. `addresses.country`) since every filter needs to be defined in the GraphQL schema. But you can define a custom filter instead:

```js
customFilters: [
{
name: '_withAddressCountry',
arguments: 'country: String',
filter: ({ input, context, filterArguments }) => {
const { country } = filterArguments;
return {
selector: { 'addresses.country': country },
options: {},
};
},
},
],
```

### Sort

```graphql
query RecentMovies {
movies(input: { filter: { year: { gte: "2010" } }, sort: { year: "desc" } }) {
results{
_id
title
year
}
}
}
```

#### Custom Sorts

Custom sorts are not yet implemented, but you can modify the `options` property returned by a custom filter to achieve the same effect.

### Search

In some cases you'll want to select data based on a field value, but without knowing exactly which field to search in. While you can build a complex query that lists every field needing to be searched, Vulcan also offers a shortcut in the form of the `search` argument:

```gql
query FightMovies {
movies(input: { search: "fight" ) {
results{
_id
title
year
description
}
}
}
```

On the server, Vulcan will search any field marked as `searchable: true` in its collection's schema for the string `fight` and will return the result.

## Single-Document Queries Arguments

The following arguments are only available for single-document queries.

### ID

Sometimes you already know the ID of the specific document you want to select. In those cases, you can use the `id` argument:

```gql
query myMovie {
movie(input: { id: "123foo" }) {
result{
_id
title
year
}
}
}
```

## Multi Document Queries Arguments

The following arguments are only available for multi-document queries.


### Limit

```gql
query RecentMovies {
movies(input: { filter: { year: { gte: 2010" } }, sort: { year: "desc" }, limit: 10 }) {
results{
_id
title
year
}
}
}
```
### Offset
```gql
query NextPageOfMovies {
movies(input: { offset: 10, limit: 10 }) {
results{
_id
title
year
}
}
}
```
1 change: 1 addition & 0 deletions docusaurus/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const sidebars = {
items: [
"vulcan-fire/get-started",
"vulcan-fire/groups-permissions",
"vulcan-fire/filtering",
"vulcan-fire/customTopLevelResolvers",
"vulcan-fire/customFieldResolvers",
"vulcan-fire/outsideGraphql",
Expand Down
2 changes: 2 additions & 0 deletions packages/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export * from "./fragments/typings";
export * from "./fragments/utils";

export * from "./decorators";

export * from "./queries";
1 change: 1 addition & 0 deletions packages/graphql/queries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./multi";
32 changes: 32 additions & 0 deletions packages/graphql/queries/multi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { DocumentNode } from "graphql";
import gql from "graphql-tag";
import { multiClientTemplate } from "../templates";
import type { VulcanGraphqlModel } from "../typings";

interface BuildMultiQueryArgs {
model: VulcanGraphqlModel;
fragmentName?: string;
fragment?: string | DocumentNode;
extraQueries?: string;
}

/**
* Graphql Query for getting multiple documents
*/
export const multiQuery = ({
model,
fragmentName = model.graphql.defaultFragmentName,
fragment = model.graphql.defaultFragment,
extraQueries,
}: BuildMultiQueryArgs) => {
const { typeName, multiTypeName } = model.graphql;
return gql`
${multiClientTemplate({
typeName,
multiTypeName,
fragmentName,
extraQueries,
})}
${fragment}
`;
};
2 changes: 1 addition & 1 deletion packages/graphql/server/defaultSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ input String_Selector {
_eq: String
_gt: String
_gte: String
#_ilike: String
_in: [String!]
_is_null: Boolean
_like: String
_lt: String
_lte: String
_neq: String
#_ilike: String
#_nilike: String
#_nin: [String!]
#_nlike: String
Expand Down
12 changes: 11 additions & 1 deletion packages/mongo-apollo/objectIdScalar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ export const GraphqlObjectId = "GraphQLObjectId";
* NOTE: the type is ObjectID to be consistent with GraphQL "ID" type
* Be careful with the casing
*/
export const objectIdTypeDefs = `scalar ${GraphqlObjectId}`;
export const objectIdTypeDefs = `
scalar ${GraphqlObjectId}
# Inspired by the String_Selector
input GraphQLObjectId_Selector {
_eq: ${GraphQLObjectId}
_in: [${GraphQLObjectId}!]
_is_null: Boolean
_neq: ${GraphqlObjectId}
}
`;

export const objectIdResolvers = {
GraphQLObjectId,
Expand Down
4 changes: 2 additions & 2 deletions packages/mongo/mongoParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ const conversionTable = {
_lte: "$lte",
_neq: "$ne",
_nin: "$nin",
asc: 1,
desc: -1,
_is_null: (value) => ({ $exists: !value }),
_is: (value) => ({ $elemMatch: { $eq: value } }),
_contains: (value) => ({ $elemMatch: { $eq: value } }),
asc: 1,
desc: -1,
_like: (value) => ({
$regex: value,
$options: "i",
Expand Down
28 changes: 2 additions & 26 deletions packages/react-hooks/multi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ Differences with Vulcan Meteor:

import { DocumentNode } from "graphql";
import { useQuery, QueryResult, QueryHookOptions } from "@apollo/client";
import gql from "graphql-tag";
import { useState } from "react";
import {
multiClientTemplate,
VulcanGraphqlModel,
MultiVariables,
MultiInput,
multiQuery,
} from "@vulcanjs/graphql";
import merge from "lodash/merge.js";
import get from "lodash/get.js";
Expand All @@ -30,29 +29,6 @@ const defaultInput = {
enableCache: false,
};

interface BuildMultiQueryArgs {
model: VulcanGraphqlModel;
fragmentName?: string;
fragment?: string | DocumentNode;
extraQueries?: string;
}
export const buildMultiQuery = ({
model,
fragmentName = model.graphql.defaultFragmentName,
fragment = model.graphql.defaultFragment,
extraQueries,
}: BuildMultiQueryArgs) => {
const { typeName, multiTypeName } = model.graphql;
return gql`
${multiClientTemplate({
typeName,
multiTypeName,
fragmentName,
extraQueries,
})}
${fragment}
`;
};
const getInitialPaginationInput = (options, props) => {
// get initial limit from props, or else options, or else default value
const limit =
Expand Down Expand Up @@ -259,7 +235,7 @@ export const useMulti = <TModel = any, TData = any>(
const { multiResolverName: resolverName } = model.graphql;

// build graphql query from options
const query = buildMultiQuery({
const query = multiQuery({
model,
fragmentName,
extraQueries,
Expand Down
Loading

0 comments on commit be29b8b

Please sign in to comment.