Skip to content

Commit d19969d

Browse files
committed
feat: add searchPagination resolver
1 parent b2d761b commit d19969d

File tree

3 files changed

+127
-4
lines changed

3 files changed

+127
-4
lines changed

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,15 @@ Live demo of [Introspection of Elasticsearch API via Graphiql](https://graphql-c
5252
---
5353

5454
## TypeComposer from Elastic mapping
55-
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).
55+
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).
5656

5757
Generated TypeComposer model has several awesome resolvers:
5858
- `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.
5959
- `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.
60-
- more resolvers will be later after my vacation: `suggest`, `getById`, `updateById` and others
60+
- `searchPagination` - elastic `search` method that has `page` and `perPage` arguments
61+
- `findById` - get elastic record by id
62+
- `updateById` - update elastic record by id
63+
- feel free to add your resolver or ask for a new one
6164

6265
```js
6366
import { GraphQLSchema, GraphQLObjectType } from 'graphql';
@@ -118,8 +121,9 @@ const Schema = new GraphQLSchema({
118121
query: new GraphQLObjectType({
119122
name: 'Query',
120123
fields: {
121-
user: UserTC.get('$search').getFieldConfig(),
122-
userConnection: UserTC.get('$searchConnection').getFieldConfig(),
124+
user: UserTC.getResolver('search').getFieldConfig(),
125+
userPagination: UserTC.getResolver('searchPagination').getFieldConfig(),
126+
userConnection: UserTC.getResolver('searchConnection').getFieldConfig(),
123127
},
124128
}),
125129
});

src/composeWithElastic.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { TypeComposer } from 'graphql-compose';
44
import { convertToSourceTC, inputPropertiesToGraphQLTypes } from './mappingConverter';
55
import createSearchResolver from './resolvers/search';
66
import createSearchConnectionResolver from './resolvers/searchConnection';
7+
import createSearchPaginationResolver from './resolvers/searchPagination';
78
import createFindByIdResolver from './resolvers/findById';
89
import createUpdateByIdResolver from './resolvers/updateById';
910

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

6667
const searchR = createSearchResolver(fieldMap, sourceTC, opts);
6768
const searchConnectionR = createSearchConnectionResolver(searchR, opts);
69+
const searchPaginationR = createSearchPaginationResolver(searchR, opts);
6870
const findByIdR = createFindByIdResolver(fieldMap, sourceTC, opts);
6971
const updateByIdR = createUpdateByIdResolver(fieldMap, sourceTC, opts);
7072

7173
sourceTC.addResolver(searchR);
7274
sourceTC.addResolver(searchConnectionR);
75+
sourceTC.addResolver(searchPaginationR);
7376
sourceTC.addResolver(findByIdR);
7477
sourceTC.addResolver(updateByIdR);
7578

src/resolvers/searchPagination.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/* @flow */
2+
3+
import { Resolver, TypeComposer } from 'graphql-compose';
4+
import { getTypeName, getOrSetType } from '../utils';
5+
6+
export default function createSearchPaginationResolver(
7+
searchResolver: Resolver,
8+
opts: mixed = {}
9+
): Resolver {
10+
const resolver = searchResolver.clone({
11+
name: `searchPagination`,
12+
});
13+
14+
resolver
15+
.addArgs({
16+
page: 'Int',
17+
perPage: {
18+
type: 'Int',
19+
defaultValue: 20,
20+
},
21+
})
22+
.removeArg(['limit', 'skip'])
23+
.reorderArgs(['q', 'query', 'sort', 'aggs', 'page', 'perPage']);
24+
25+
const searchTC = searchResolver.getTypeComposer();
26+
if (!searchTC) {
27+
throw new Error('Cannot get TypeComposer from resolver. Maybe resolver return Scalar?!');
28+
}
29+
30+
const typeName = searchTC.getTypeName();
31+
resolver.setType(
32+
searchTC
33+
.clone(`${typeName}Pagination`)
34+
.addFields({
35+
pageInfo: getPageInfoTC(opts),
36+
items: [searchTC.get('hits')],
37+
})
38+
.removeField('hits')
39+
.reorderFields(['items', 'count', 'pageInfo', 'aggregations'])
40+
);
41+
42+
resolver.resolve = async rp => {
43+
const { args = {}, projection = {} } = rp;
44+
45+
const page = args.page || 1;
46+
if (page <= 0) {
47+
throw new Error('Argument `page` should be positive number.');
48+
}
49+
const perPage = args.perPage || 20;
50+
if (perPage <= 0) {
51+
throw new Error('Argument `perPage` should be positive number.');
52+
}
53+
delete args.page;
54+
delete args.perPage;
55+
args.limit = perPage;
56+
args.skip = (page - 1) * perPage;
57+
58+
if (projection.items) {
59+
projection.hits = projection.items;
60+
delete projection.items;
61+
}
62+
63+
const res = await searchResolver.resolve(rp);
64+
65+
const items = res.hits || [];
66+
const itemCount = res.count || 0;
67+
68+
const result = {
69+
...res,
70+
pageInfo: {
71+
hasNextPage: itemCount > page * perPage,
72+
hasPreviousPage: page > 1,
73+
currentPage: page,
74+
perPage,
75+
pageCount: Math.ceil(itemCount / perPage),
76+
itemCount,
77+
},
78+
items,
79+
};
80+
81+
return result;
82+
};
83+
84+
return resolver;
85+
}
86+
87+
function getPageInfoTC(opts: mixed = {}): TypeComposer {
88+
const name = getTypeName('PaginationInfo', opts);
89+
90+
return getOrSetType(name, () =>
91+
TypeComposer.create(
92+
`
93+
# Information about pagination.
94+
type ${name} {
95+
# Current page number
96+
currentPage: Int!
97+
98+
# Number of items per page
99+
perPage: Int!
100+
101+
# Total number of pages
102+
pageCount: Int
103+
104+
# Total number of items
105+
itemCount: Int
106+
107+
# When paginating forwards, are there more items?
108+
hasNextPage: Boolean
109+
110+
# When paginating backwards, are there more items?
111+
hasPreviousPage: Boolean
112+
}
113+
`
114+
)
115+
);
116+
}

0 commit comments

Comments
 (0)