Skip to content

Commit

Permalink
feat: add searchPagination resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
nodkz committed Sep 10, 2018
1 parent b2d761b commit d19969d
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 4 deletions.
12 changes: 8 additions & 4 deletions README.md
Expand Up @@ -52,12 +52,15 @@ Live demo of [Introspection of Elasticsearch API via Graphiql](https://graphql-c
---

## TypeComposer from Elastic mapping
In other side this module is a plugin for [graphql-compose](https://github.com/nodkz/graphql-compose), which derives GraphQLType from your [elastic mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html) generates tons of types, provides all available methods in QueryDSL, Aggregations, Sorting with field autocompletion according to types in your mapping (like Dev Tools Console in Kibana).
In other side this module is a plugin for [graphql-compose](https://github.com/graphql-compose/graphql-compose), which derives GraphQLType from your [elastic mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html) generates tons of types, provides all available methods in QueryDSL, Aggregations, Sorting with field autocompletion according to types in your mapping (like Dev Tools Console in Kibana).

Generated TypeComposer model has several awesome resolvers:
- `search` - greatly simplified elastic `search` method. According to GraphQL adaptation and its projection bunch of params setup automatically due your graphql query (eg `_source`, `explain`, `version`, `trackScores`), other rare fine tuning params moved to `opts` input field.
- `searchConnection` - elastic `search` method that implements Relay Cursor Connection [spec](https://facebook.github.io/relay/graphql/connections.htm) for infinite lists. Internally it uses cheap [search_after](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-search-after.html) API. One downside, Elastic does not support backward scrolling, so `before` argument will not work.
- more resolvers will be later after my vacation: `suggest`, `getById`, `updateById` and others
- `searchPagination` - elastic `search` method that has `page` and `perPage` arguments
- `findById` - get elastic record by id
- `updateById` - update elastic record by id
- feel free to add your resolver or ask for a new one

```js
import { GraphQLSchema, GraphQLObjectType } from 'graphql';
Expand Down Expand Up @@ -118,8 +121,9 @@ const Schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
user: UserTC.get('$search').getFieldConfig(),
userConnection: UserTC.get('$searchConnection').getFieldConfig(),
user: UserTC.getResolver('search').getFieldConfig(),
userPagination: UserTC.getResolver('searchPagination').getFieldConfig(),
userConnection: UserTC.getResolver('searchConnection').getFieldConfig(),
},
}),
});
Expand Down
3 changes: 3 additions & 0 deletions src/composeWithElastic.js
Expand Up @@ -4,6 +4,7 @@ import { TypeComposer } from 'graphql-compose';
import { convertToSourceTC, inputPropertiesToGraphQLTypes } from './mappingConverter';
import createSearchResolver from './resolvers/search';
import createSearchConnectionResolver from './resolvers/searchConnection';
import createSearchPaginationResolver from './resolvers/searchPagination';
import createFindByIdResolver from './resolvers/findById';
import createUpdateByIdResolver from './resolvers/updateById';

Expand Down Expand Up @@ -65,11 +66,13 @@ export function composeWithElastic(opts: composeWithElasticOptsT): TypeComposer

const searchR = createSearchResolver(fieldMap, sourceTC, opts);
const searchConnectionR = createSearchConnectionResolver(searchR, opts);
const searchPaginationR = createSearchPaginationResolver(searchR, opts);
const findByIdR = createFindByIdResolver(fieldMap, sourceTC, opts);
const updateByIdR = createUpdateByIdResolver(fieldMap, sourceTC, opts);

sourceTC.addResolver(searchR);
sourceTC.addResolver(searchConnectionR);
sourceTC.addResolver(searchPaginationR);
sourceTC.addResolver(findByIdR);
sourceTC.addResolver(updateByIdR);

Expand Down
116 changes: 116 additions & 0 deletions src/resolvers/searchPagination.js
@@ -0,0 +1,116 @@
/* @flow */

import { Resolver, TypeComposer } from 'graphql-compose';
import { getTypeName, getOrSetType } from '../utils';

export default function createSearchPaginationResolver(
searchResolver: Resolver,
opts: mixed = {}
): Resolver {
const resolver = searchResolver.clone({
name: `searchPagination`,
});

resolver
.addArgs({
page: 'Int',
perPage: {
type: 'Int',
defaultValue: 20,
},
})
.removeArg(['limit', 'skip'])
.reorderArgs(['q', 'query', 'sort', 'aggs', 'page', 'perPage']);

const searchTC = searchResolver.getTypeComposer();
if (!searchTC) {
throw new Error('Cannot get TypeComposer from resolver. Maybe resolver return Scalar?!');
}

const typeName = searchTC.getTypeName();
resolver.setType(
searchTC
.clone(`${typeName}Pagination`)
.addFields({
pageInfo: getPageInfoTC(opts),
items: [searchTC.get('hits')],
})
.removeField('hits')
.reorderFields(['items', 'count', 'pageInfo', 'aggregations'])
);

resolver.resolve = async rp => {
const { args = {}, projection = {} } = rp;

const page = args.page || 1;
if (page <= 0) {
throw new Error('Argument `page` should be positive number.');
}
const perPage = args.perPage || 20;
if (perPage <= 0) {
throw new Error('Argument `perPage` should be positive number.');
}
delete args.page;
delete args.perPage;
args.limit = perPage;
args.skip = (page - 1) * perPage;

if (projection.items) {
projection.hits = projection.items;
delete projection.items;
}

const res = await searchResolver.resolve(rp);

const items = res.hits || [];
const itemCount = res.count || 0;

const result = {
...res,
pageInfo: {
hasNextPage: itemCount > page * perPage,
hasPreviousPage: page > 1,
currentPage: page,
perPage,
pageCount: Math.ceil(itemCount / perPage),
itemCount,
},
items,
};

return result;
};

return resolver;
}

function getPageInfoTC(opts: mixed = {}): TypeComposer {
const name = getTypeName('PaginationInfo', opts);

return getOrSetType(name, () =>
TypeComposer.create(
`
# Information about pagination.
type ${name} {
# Current page number
currentPage: Int!
# Number of items per page
perPage: Int!
# Total number of pages
pageCount: Int
# Total number of items
itemCount: Int
# When paginating forwards, are there more items?
hasNextPage: Boolean
# When paginating backwards, are there more items?
hasPreviousPage: Boolean
}
`
)
);
}

0 comments on commit d19969d

Please sign in to comment.