Skip to content

Commit c2b2d60

Browse files
committed
wip: Simplify input args and output for search and searchConnection resolvers
1 parent 527df15 commit c2b2d60

File tree

4 files changed

+174
-64
lines changed

4 files changed

+174
-64
lines changed

examples/elastic50/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ server.use(
8484
// log: 'trace',
8585
// }),
8686
},
87+
formatError: error => ({
88+
message: error.message,
89+
stack: error.stack.split('\n'),
90+
}),
8791
})
8892
);
8993

src/resolvers/search.js

Lines changed: 109 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
/* @flow */
22
/* eslint-disable no-param-reassign */
33

4-
import { Resolver, TypeComposer, isObject } from 'graphql-compose';
5-
import type { ResolveParams, ProjectionType } from 'graphql-compose/lib/definition';
4+
import {
5+
Resolver,
6+
TypeComposer,
7+
InputTypeComposer,
8+
isObject,
9+
} from 'graphql-compose';
10+
import type {
11+
ResolveParams,
12+
ProjectionType,
13+
} from 'graphql-compose/lib/definition';
614
import type { FieldsMapByElasticType } from '../mappingConverter';
715
import ElasticApiParser from '../ElasticApiParser';
816
import type { ElasticApiVersion } from '../ElasticApiParser';
@@ -20,7 +28,7 @@ export type ElasticSearchResolverOpts = {
2028

2129
export default function createSearchResolver(
2230
fieldMap: FieldsMapByElasticType,
23-
sourceTC?: TypeComposer,
31+
sourceTC: TypeComposer,
2432
elasticClient?: mixed,
2533
opts?: ElasticSearchResolverOpts = {}
2634
): Resolver<*, *> {
@@ -43,10 +51,6 @@ export default function createSearchResolver(
4351
version: opts.elasticApiVersion || '5_0',
4452
prefix,
4553
});
46-
const searchFC = parser.generateFieldConfig('search', {
47-
index: 'cv',
48-
type: 'cv',
49-
});
5054

5155
const searchITC = getSearchBodyITC({
5256
prefix,
@@ -55,6 +59,11 @@ export default function createSearchResolver(
5559

5660
searchITC.removeField(['size', 'from', '_source', 'explain', 'version']);
5761

62+
const searchFC = parser.generateFieldConfig('search', {
63+
index: 'cv',
64+
type: 'cv',
65+
});
66+
5867
const argsConfigMap = Object.assign({}, searchFC.args, {
5968
body: {
6069
type: searchITC.getType(),
@@ -68,31 +77,69 @@ export default function createSearchResolver(
6877
delete argsConfigMap._source; // added automatically due projection
6978
delete argsConfigMap._sourceExclude; // added automatically due projection
7079
delete argsConfigMap._sourceInclude; // added automatically due projection
80+
delete argsConfigMap.trackScores; // added automatically due projection (is _scrore requested with sort)
7181

7282
delete argsConfigMap.size;
7383
delete argsConfigMap.from;
84+
// $FlowFixMe
7485
argsConfigMap.limit = 'Int';
86+
// $FlowFixMe
7587
argsConfigMap.skip = 'Int';
7688

77-
const type = getSearchOutputTC({ prefix, fieldMap, sourceTC });
78-
// $FlowFixMe
79-
type.addFields({
80-
count: 'Int',
81-
max_score: 'Float',
89+
const bodyITC = InputTypeComposer.create(argsConfigMap.body.type);
90+
argsConfigMap.query = bodyITC.getField('query');
91+
argsConfigMap.aggs = bodyITC.getField('aggs');
92+
argsConfigMap.highlight = bodyITC.getField('highlight');
93+
94+
const topLevelArgs = [
95+
'limit',
96+
'skip',
97+
'q',
98+
'opts',
99+
'query',
100+
'aggs',
101+
'highlight',
102+
];
103+
argsConfigMap.opts = InputTypeComposer.create({
104+
name: `${sourceTC.getTypeName()}Opts`,
105+
fields: Object.assign({}, argsConfigMap),
106+
}).removeField(topLevelArgs);
107+
Object.keys(argsConfigMap).forEach(argKey => {
108+
if (!topLevelArgs.includes(argKey)) {
109+
delete argsConfigMap[argKey];
110+
}
82111
});
83112

113+
const type = getSearchOutputTC({ prefix, fieldMap, sourceTC });
114+
type
115+
.addFields({
116+
// $FlowFixMe
117+
count: 'Int',
118+
// $FlowFixMe
119+
max_score: 'Float',
120+
// $FlowFixMe
121+
hits: [type.get('hits.hits')],
122+
})
123+
.reorderFields([
124+
'hits',
125+
'count',
126+
'aggregations',
127+
'max_score',
128+
'took',
129+
'timed_out',
130+
'_shards',
131+
]);
132+
84133
// $FlowFixMe
85134
return new Resolver({
86135
type,
87136
name: 'search',
88137
kind: 'query',
89138
args: argsConfigMap,
90139
resolve: async (rp: ResolveParams<*, *>) => {
91-
const { args = {}, projection = {} } = rp;
92-
93-
if (args.body) {
94-
args.body = prepareBodyInResolve(args.body, fieldMap);
95-
}
140+
let args: Object = rp.args || {};
141+
const projection = rp.projection || {};
142+
if (!args.body) args.body = {};
96143

97144
if ({}.hasOwnProperty.call(args, 'limit')) {
98145
args.size = args.limit;
@@ -105,47 +152,78 @@ export default function createSearchResolver(
105152
}
106153

107154
const { hits = {} } = projection;
108-
// $FlowFixMe
109-
const { hits: hitsHits } = hits;
110155

111-
if (typeof hitsHits === 'object') {
156+
if (typeof hits === 'object') {
112157
// Turn on explain if in projection requested this fields:
113-
if (hitsHits._shard || hitsHits._node || hitsHits._explanation) {
114-
// $FlowFixMe
158+
if (hits._shard || hits._node || hits._explanation) {
115159
args.body.explain = true;
116160
}
117161

118-
if (hitsHits._version) {
119-
// $FlowFixMe
162+
if (hits._version) {
120163
args.body.version = true;
121164
}
122165

123-
if (!hitsHits._source) {
124-
// $FlowFixMe
166+
if (!hits._source) {
125167
args.body._source = false;
126168
} else {
127169
// $FlowFixMe
128-
args.body._source = toDottedList(hitsHits._source);
170+
args.body._source = toDottedList(hits._source);
129171
}
172+
173+
if (hits._score) {
174+
args.body.track_scores = true;
175+
}
176+
}
177+
178+
if (args.query) {
179+
args.body.query = args.query;
180+
delete args.query;
181+
}
182+
183+
if (args.aggs) {
184+
args.body.aggs = args.aggs;
185+
delete args.aggs;
186+
}
187+
188+
if (args.opts) {
189+
args = {
190+
...args.opts,
191+
...args,
192+
body: { ...args.opts.body, ...args.body },
193+
};
194+
delete args.opts;
195+
}
196+
197+
if (args.body) {
198+
args.body = prepareBodyInResolve(args.body, fieldMap);
130199
}
131200

132201
// $FlowFixMe
133-
const res = await searchFC.resolve(rp.source, args, rp.context, rp.info);
202+
const res: any = await searchFC.resolve(
203+
rp.source,
204+
args,
205+
rp.context,
206+
rp.info
207+
);
134208

135209
res.count = res.hits.total;
136210
res.max_score = res.hits.max_score;
211+
res.hits = res.hits.hits;
137212

138213
return res;
139214
},
140-
});
215+
}).reorderArgs(['q', 'query', 'aggs', 'limit', 'skip']);
141216
}
142217

143-
export function toDottedList(projection: ProjectionType, prev: string[]): string[] | boolean {
218+
export function toDottedList(
219+
projection: ProjectionType,
220+
prev?: string[]
221+
): string[] | boolean {
144222
let result = [];
145223
Object.keys(projection).forEach(k => {
146224
if (isObject(projection[k])) {
147225
// $FlowFixMe
148-
const tmp = toDottedList(projection[k], prev ? [ ...prev, k] : [k]);
226+
const tmp = toDottedList(projection[k], prev ? [...prev, k] : [k]);
149227
if (Array.isArray(tmp)) {
150228
result = result.concat(tmp);
151229
return;

src/resolvers/searchConnection.js

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* @Flow */
22

3-
import { Resolver, TypeComposer, isObject } from 'graphql-compose';
3+
import { Resolver, TypeComposer } from 'graphql-compose';
44
import { getTypeName, getOrSetType } from '../utils';
55

66
export default function createSearchConnectionResolver(
@@ -11,32 +11,36 @@ export default function createSearchConnectionResolver(
1111
name: `${searchResolver.name}Connection`,
1212
});
1313

14-
resolver.addArgs({
15-
first: 'Int',
16-
after: 'String',
17-
last: 'Int',
18-
before: 'String',
19-
});
20-
resolver.removeArg('limit');
21-
resolver.removeArg('skip');
14+
resolver
15+
.addArgs({
16+
first: 'Int',
17+
after: 'String',
18+
last: 'Int',
19+
before: 'String',
20+
})
21+
.removeArg(['limit', 'skip'])
22+
.reorderArgs(['q', 'query', 'aggs', 'first', 'after', 'last', 'before']);
2223

2324
const searchType = searchResolver.getTypeComposer();
2425
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);
26+
resolver.setType(
27+
searchType
28+
.clone(`${typeName}Connection`)
29+
.addFields({
30+
pageInfo: getPageInfoTC(opts),
31+
edges: [
32+
TypeComposer.create({
33+
name: `${typeName}Edge`,
34+
fields: {
35+
node: searchType.get('hits'),
36+
cursor: 'String!',
37+
},
38+
}),
39+
],
40+
})
41+
.removeField('hits')
42+
.reorderFields(['count', 'pageInfo', 'edges', 'aggregations'])
43+
);
4044

4145
resolver.resolve = async rp => {
4246
const { args = {} } = rp;
@@ -49,21 +53,43 @@ export default function createSearchConnectionResolver(
4953
if (last < 0) {
5054
throw new Error('Argument `last` should be non-negative number.');
5155
}
56+
const { before, after } = args;
57+
delete args.before;
58+
delete args.after;
59+
if (before !== undefined) {
60+
throw new Error('Elastic does not support before cursor.');
61+
}
62+
if (after) {
63+
if (!args.body) args.body = {};
64+
const tmp = cursorToData(after);
65+
if (Array.isArray(tmp)) {
66+
args.body.search_after = tmp;
67+
}
68+
}
5269

53-
let limit = last || first || 20;
54-
let skip = last > 0 ? first - last : 0;
70+
const limit = last || first || 20;
71+
const skip = last > 0 ? first - last : 0;
5572

56-
delete rp.args.last;
57-
delete rp.args.first;
58-
rp.args.limit = limit;
59-
rp.args.skip = skip;
73+
delete args.last;
74+
delete args.first;
75+
args.limit = limit + 1; // +1 document, to check next page presence
76+
args.skip = skip;
6077

6178
const res = await searchResolver.resolve(rp);
62-
const list = res.hits.hits;
79+
80+
let list = res.hits.hits;
81+
const hasExtraRecords = list.length > limit;
82+
if (hasExtraRecords) list = list.slice(0, limit);
83+
const edges = list.map(node => ({ node, cursor: dataToCursor(node.sort) }));
6384
const result = {
6485
...res,
65-
pageInfo: {}, // TODO <------------------
66-
edges: list.map(node => ({ node, cursor: dataToCursor(node.sort) })),
86+
pageInfo: {
87+
hasNextPage: limit > 0 && hasExtraRecords,
88+
hasPreviousPage: false, // Elastic does not support before cursor
89+
startCursor: edges.length > 0 ? edges[0].cursor : null,
90+
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null,
91+
},
92+
edges,
6793
};
6894

6995
return result;
@@ -116,5 +142,6 @@ export function cursorToData(cursor: string): mixed {
116142
}
117143

118144
export function dataToCursor(data: mixed): string {
145+
if (!data) return '';
119146
return base64(JSON.stringify(data));
120147
}

src/types/SearchOutput.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function getSearchOutputTC(opts: SearchOptsT = {}): TypeComposer {
3535
hits: [getSearchHitItemTC(opts)],
3636
},
3737
})),
38+
aggregations: 'JSON',
3839
},
3940
}));
4041
}

0 commit comments

Comments
 (0)