Skip to content

Commit 527df15

Browse files
committed
wip(SearchConnection): Create SearchConnectionResolver wrapper on SearchResolver
1 parent a2b6586 commit 527df15

File tree

5 files changed

+228
-63
lines changed

5 files changed

+228
-63
lines changed

examples/elastic50/index.js

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';
77
import elasticsearch from 'elasticsearch';
88
import ElasticApiParser from '../../src/ElasticApiParser'; // or import { ElasticApiParser } from 'graphql-compose-elasticsearch';
99
import createSearchResolver from '../../src/resolvers/search';
10+
import createSearchConnectionResolver
11+
from '../../src/resolvers/searchConnection';
1012
import fieldMap from '../../src/__mocks__/cvMapping';
1113
import {
1214
inputPropertiesToGraphQLTypes,
@@ -15,23 +17,27 @@ import {
1517

1618
const expressPort = process.env.port || process.env.PORT || 9201;
1719

20+
const cvSearch = createSearchResolver(
21+
inputPropertiesToGraphQLTypes(fieldMap),
22+
convertToSourceTC(fieldMap, 'Cv', { prefix: '' }),
23+
new elasticsearch.Client({
24+
host: 'http://localhost:9200',
25+
apiVersion: '5.0',
26+
log: 'trace',
27+
}),
28+
{
29+
prefix: 'Cv',
30+
}
31+
);
32+
const cvSearchConnection = createSearchConnectionResolver(cvSearch);
33+
1834
const generatedSchema = new GraphQLSchema({
1935
query: new GraphQLObjectType({
2036
name: 'Query',
2137
fields: {
2238
// $FlowFixMe
23-
cv: createSearchResolver(
24-
inputPropertiesToGraphQLTypes(fieldMap),
25-
convertToSourceTC(fieldMap, 'Cv', { prefix: '' }),
26-
new elasticsearch.Client({
27-
host: 'http://localhost:9200',
28-
apiVersion: '5.0',
29-
log: 'trace',
30-
}),
31-
{
32-
prefix: 'Cv',
33-
}
34-
).getFieldConfig(),
39+
cv: cvSearch.getFieldConfig(),
40+
cvConnection: cvSearchConnection.getFieldConfig(),
3541
// see node_modules/elasticsearch/src/lib/apis/ for available versions
3642
elastic50: {
3743
description: 'Elastic v5.0',

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,17 @@
3838
"babel-preset-env": "^1.2.2",
3939
"cz-conventional-changelog": "^2.0.0",
4040
"elasticsearch": "^12.1.3",
41-
"eslint": "^3.17.1",
41+
"eslint": "^3.18.0",
4242
"eslint-config-airbnb-base": "^11.0.1",
4343
"eslint-config-prettier": "^1.0.2",
4444
"eslint-plugin-flowtype": "^2.30.3",
4545
"eslint-plugin-import": "^2.2.0",
4646
"eslint-plugin-prettier": "^2.0.0",
4747
"express": "^4.15.2",
4848
"express-graphql": "^0.6.3",
49-
"flow-bin": "^0.41.0",
49+
"flow-bin": "^0.42.0",
5050
"graphql": "^0.9.1",
51-
"graphql-compose": "^1.17.2",
51+
"graphql-compose": "^1.17.3",
5252
"jest": "^19.0.2",
5353
"jest-babel": "^1.0.1",
5454
"npm-run-all": "^4.0.1",

src/resolvers/search.js

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ export default function createSearchResolver(
3030
);
3131
}
3232

33-
// if (!(tc instanceof TypeComposer)) {
34-
// throw new Error(
35-
// 'Second arg for Resolver search() should be instance of TypeComposer.'
36-
// );
37-
// }
33+
if (!(sourceTC instanceof TypeComposer)) {
34+
throw new Error(
35+
'Second arg for Resolver search() should be instance of TypeComposer.'
36+
);
37+
}
3838

3939
const prefix = opts.prefix || 'Es';
4040

@@ -48,36 +48,62 @@ export default function createSearchResolver(
4848
type: 'cv',
4949
});
5050

51-
const args = Object.assign({}, searchFC.args, {
51+
const searchITC = getSearchBodyITC({
52+
prefix,
53+
fieldMap,
54+
});
55+
56+
searchITC.removeField(['size', 'from', '_source', 'explain', 'version']);
57+
58+
const argsConfigMap = Object.assign({}, searchFC.args, {
5259
body: {
53-
type: getSearchBodyITC({
54-
prefix,
55-
fieldMap,
56-
}).getType(),
60+
type: searchITC.getType(),
5761
},
5862
});
5963

60-
delete args.index; // index can not be changed, it hardcoded in searchFC
61-
delete args.type; // type can not be changed, it hardcoded in searchFC
62-
delete args.explain; // added automatically if requested _shard, _node, _explanation
63-
delete args.version; // added automatically if requested _version
64-
delete args._source; // added automatically due projection
65-
delete args._sourceExclude; // added automatically due projection
66-
delete args._sourceInclude; // added automatically due projection
64+
delete argsConfigMap.index; // index can not be changed, it hardcoded in searchFC
65+
delete argsConfigMap.type; // type can not be changed, it hardcoded in searchFC
66+
delete argsConfigMap.explain; // added automatically if requested _shard, _node, _explanation
67+
delete argsConfigMap.version; // added automatically if requested _version
68+
delete argsConfigMap._source; // added automatically due projection
69+
delete argsConfigMap._sourceExclude; // added automatically due projection
70+
delete argsConfigMap._sourceInclude; // added automatically due projection
71+
72+
delete argsConfigMap.size;
73+
delete argsConfigMap.from;
74+
argsConfigMap.limit = 'Int';
75+
argsConfigMap.skip = 'Int';
76+
77+
const type = getSearchOutputTC({ prefix, fieldMap, sourceTC });
78+
// $FlowFixMe
79+
type.addFields({
80+
count: 'Int',
81+
max_score: 'Float',
82+
});
6783

6884
// $FlowFixMe
6985
return new Resolver({
70-
// $FlowFixMe
71-
type: sourceTC ? getSearchOutputTC({ prefix, fieldMap, sourceTC }) : 'JSON',
86+
type,
7287
name: 'search',
7388
kind: 'query',
74-
args,
75-
resolve: (rp: ResolveParams<*, *>) => {
76-
if (rp.args && rp.args.body) {
77-
rp.args.body = prepareBodyInResolve(rp.args.body, fieldMap);
89+
args: argsConfigMap,
90+
resolve: async (rp: ResolveParams<*, *>) => {
91+
const { args = {}, projection = {} } = rp;
92+
93+
if (args.body) {
94+
args.body = prepareBodyInResolve(args.body, fieldMap);
95+
}
96+
97+
if ({}.hasOwnProperty.call(args, 'limit')) {
98+
args.size = args.limit;
99+
delete args.limit;
100+
}
101+
102+
if ({}.hasOwnProperty.call(args, 'skip')) {
103+
args.from = args.skip;
104+
delete args.skip;
78105
}
79106

80-
const { projection = {} } = rp;
81107
const { hits = {} } = projection;
82108
// $FlowFixMe
83109
const { hits: hitsHits } = hits;
@@ -86,25 +112,28 @@ export default function createSearchResolver(
86112
// Turn on explain if in projection requested this fields:
87113
if (hitsHits._shard || hitsHits._node || hitsHits._explanation) {
88114
// $FlowFixMe
89-
rp.args.body.explain = true;
115+
args.body.explain = true;
90116
}
91117

92118
if (hitsHits._version) {
93119
// $FlowFixMe
94-
rp.args.body.version = true;
120+
args.body.version = true;
95121
}
96122

97123
if (!hitsHits._source) {
98124
// $FlowFixMe
99-
rp.args.body._source = false;
125+
args.body._source = false;
100126
} else {
101127
// $FlowFixMe
102-
rp.args.body._source = toDottedList(hitsHits._source);
128+
args.body._source = toDottedList(hitsHits._source);
103129
}
104130
}
105131

106132
// $FlowFixMe
107-
const res = searchFC.resolve(rp.source, rp.args, rp.context, rp.info);
133+
const res = await searchFC.resolve(rp.source, args, rp.context, rp.info);
134+
135+
res.count = res.hits.total;
136+
res.max_score = res.hits.max_score;
108137

109138
return res;
110139
},

src/resolvers/searchConnection.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/* @Flow */
2+
3+
import { Resolver, TypeComposer, isObject } from 'graphql-compose';
4+
import { getTypeName, getOrSetType } from '../utils';
5+
6+
export default function createSearchConnectionResolver(
7+
searchResolver: Resolver<*, *>,
8+
opts: mixed = {}
9+
): Resolver<*, *> {
10+
const resolver = searchResolver.clone({
11+
name: `${searchResolver.name}Connection`,
12+
});
13+
14+
resolver.addArgs({
15+
first: 'Int',
16+
after: 'String',
17+
last: 'Int',
18+
before: 'String',
19+
});
20+
resolver.removeArg('limit');
21+
resolver.removeArg('skip');
22+
23+
const searchType = searchResolver.getTypeComposer();
24+
const typeName = searchType.getTypeName();
25+
const type = searchType.clone(`${typeName}Connection`);
26+
type.addFields({
27+
pageInfo: getPageInfoTC(opts),
28+
edges: [
29+
TypeComposer.create({
30+
name: `${typeName}Edge`,
31+
fields: {
32+
node: searchType.get('hits.hits'),
33+
cursor: 'String!',
34+
},
35+
}),
36+
],
37+
});
38+
type.removeField('hits');
39+
resolver.setType(type);
40+
41+
resolver.resolve = async rp => {
42+
const { args = {} } = rp;
43+
44+
const first = parseInt(args.first, 10) || 0;
45+
if (first < 0) {
46+
throw new Error('Argument `first` should be non-negative number.');
47+
}
48+
const last = parseInt(args.last, 10) || 0;
49+
if (last < 0) {
50+
throw new Error('Argument `last` should be non-negative number.');
51+
}
52+
53+
let limit = last || first || 20;
54+
let skip = last > 0 ? first - last : 0;
55+
56+
delete rp.args.last;
57+
delete rp.args.first;
58+
rp.args.limit = limit;
59+
rp.args.skip = skip;
60+
61+
const res = await searchResolver.resolve(rp);
62+
const list = res.hits.hits;
63+
const result = {
64+
...res,
65+
pageInfo: {}, // TODO <------------------
66+
edges: list.map(node => ({ node, cursor: dataToCursor(node.sort) })),
67+
};
68+
69+
return result;
70+
};
71+
72+
return resolver;
73+
}
74+
75+
function getPageInfoTC(opts: mixed = {}): TypeComposer {
76+
const name = getTypeName('PageInfo', opts);
77+
78+
return getOrSetType(name, () =>
79+
TypeComposer.create(
80+
`
81+
# Information about pagination in a connection.
82+
type ${name} {
83+
# When paginating forwards, are there more items?
84+
hasNextPage: Boolean!
85+
86+
# When paginating backwards, are there more items?
87+
hasPreviousPage: Boolean!
88+
89+
# When paginating backwards, the cursor to continue.
90+
startCursor: String
91+
92+
# When paginating forwards, the cursor to continue.
93+
endCursor: String
94+
}
95+
`
96+
));
97+
}
98+
99+
export function base64(i: string): string {
100+
return new Buffer(i, 'ascii').toString('base64');
101+
}
102+
103+
export function unbase64(i: string): string {
104+
return new Buffer(i, 'base64').toString('ascii');
105+
}
106+
107+
export function cursorToData(cursor: string): mixed {
108+
if (typeof cursor === 'string') {
109+
try {
110+
return JSON.parse(unbase64(cursor)) || null;
111+
} catch (err) {
112+
return null;
113+
}
114+
}
115+
return null;
116+
}
117+
118+
export function dataToCursor(data: mixed): string {
119+
return base64(JSON.stringify(data));
120+
}

0 commit comments

Comments
 (0)