Skip to content

Commit a2b6586

Browse files
committed
wip(SearchResolver): Describing Output, autofill some arguments due projection
1 parent a989f48 commit a2b6586

File tree

6 files changed

+184
-10
lines changed

6 files changed

+184
-10
lines changed

examples/elastic50/index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import elasticsearch from 'elasticsearch';
88
import ElasticApiParser from '../../src/ElasticApiParser'; // or import { ElasticApiParser } from 'graphql-compose-elasticsearch';
99
import createSearchResolver from '../../src/resolvers/search';
1010
import fieldMap from '../../src/__mocks__/cvMapping';
11-
import { inputPropertiesToGraphQLTypes } from '../../src/mappingConverter';
11+
import {
12+
inputPropertiesToGraphQLTypes,
13+
convertToSourceTC,
14+
} from '../../src/mappingConverter';
1215

1316
const expressPort = process.env.port || process.env.PORT || 9201;
1417

@@ -19,7 +22,7 @@ const generatedSchema = new GraphQLSchema({
1922
// $FlowFixMe
2023
cv: createSearchResolver(
2124
inputPropertiesToGraphQLTypes(fieldMap),
22-
undefined,
25+
convertToSourceTC(fieldMap, 'Cv', { prefix: '' }),
2326
new elasticsearch.Client({
2427
host: 'http://localhost:9200',
2528
apiVersion: '5.0',

src/resolvers/__tests__/search-test.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Resolver } from 'graphql-compose';
2-
import createSearchResolver from '../search';
2+
import createSearchResolver, * as Search from '../search';
33
import elasticClient from '../../__mocks__/elasticClient';
44
import { CvTC, CvFieldMap } from '../../__mocks__/cv';
55

@@ -23,4 +23,13 @@ describe('search resolver', () => {
2323
});
2424
});
2525
});
26+
27+
describe('helper methods', () => {
28+
it('toDottedList()', () => {
29+
expect(
30+
Search.toDottedList({ a: { b: true, c: { e: true } }, d: true })
31+
).toEqual(['a.b', 'a.c.e', 'd']);
32+
expect(Search.toDottedList({})).toEqual(true);
33+
});
34+
});
2635
});

src/resolvers/search.js

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

4-
import { Resolver, TypeComposer } from 'graphql-compose';
5-
import type { ResolveParams } from 'graphql-compose/lib/definition';
4+
import { Resolver, TypeComposer, isObject } from 'graphql-compose';
5+
import type { ResolveParams, ProjectionType } from 'graphql-compose/lib/definition';
66
import type { FieldsMapByElasticType } from '../mappingConverter';
77
import ElasticApiParser from '../ElasticApiParser';
88
import type { ElasticApiVersion } from '../ElasticApiParser';
99
import {
1010
getSearchBodyITC,
1111
prepareBodyInResolve,
1212
} from '../elasticDSL/SearchBody';
13+
import { getSearchOutputTC } from '../types/SearchOutput';
1314

1415
export type ElasticSearchResolverOpts = {
1516
[name: string]: mixed,
@@ -19,7 +20,7 @@ export type ElasticSearchResolverOpts = {
1920

2021
export default function createSearchResolver(
2122
fieldMap: FieldsMapByElasticType,
22-
tc?: TypeComposer,
23+
sourceTC?: TypeComposer,
2324
elasticClient?: mixed,
2425
opts?: ElasticSearchResolverOpts = {}
2526
): Resolver<*, *> {
@@ -35,10 +36,12 @@ export default function createSearchResolver(
3536
// );
3637
// }
3738

39+
const prefix = opts.prefix || 'Es';
40+
3841
const parser = new ElasticApiParser({
3942
elasticClient,
4043
version: opts.elasticApiVersion || '5_0',
41-
prefix: opts.prefix || 'Es',
44+
prefix,
4245
});
4346
const searchFC = parser.generateFieldConfig('search', {
4447
index: 'cv',
@@ -48,26 +51,83 @@ export default function createSearchResolver(
4851
const args = Object.assign({}, searchFC.args, {
4952
body: {
5053
type: getSearchBodyITC({
51-
prefix: opts.prefix,
54+
prefix,
5255
fieldMap,
5356
}).getType(),
5457
},
5558
});
5659

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
67+
5768
// $FlowFixMe
5869
return new Resolver({
59-
type: 'JSON', // [tc],
70+
// $FlowFixMe
71+
type: sourceTC ? getSearchOutputTC({ prefix, fieldMap, sourceTC }) : 'JSON',
6072
name: 'search',
6173
kind: 'query',
6274
args,
6375
resolve: (rp: ResolveParams<*, *>) => {
6476
if (rp.args && rp.args.body) {
6577
rp.args.body = prepareBodyInResolve(rp.args.body, fieldMap);
6678
}
79+
80+
const { projection = {} } = rp;
81+
const { hits = {} } = projection;
82+
// $FlowFixMe
83+
const { hits: hitsHits } = hits;
84+
85+
if (typeof hitsHits === 'object') {
86+
// Turn on explain if in projection requested this fields:
87+
if (hitsHits._shard || hitsHits._node || hitsHits._explanation) {
88+
// $FlowFixMe
89+
rp.args.body.explain = true;
90+
}
91+
92+
if (hitsHits._version) {
93+
// $FlowFixMe
94+
rp.args.body.version = true;
95+
}
96+
97+
if (!hitsHits._source) {
98+
// $FlowFixMe
99+
rp.args.body._source = false;
100+
} else {
101+
// $FlowFixMe
102+
rp.args.body._source = toDottedList(hitsHits._source);
103+
}
104+
}
105+
67106
// $FlowFixMe
68107
const res = searchFC.resolve(rp.source, rp.args, rp.context, rp.info);
69-
// console.log(res);
108+
70109
return res;
71110
},
72111
});
73112
}
113+
114+
export function toDottedList(projection: ProjectionType, prev: string[]): string[] | boolean {
115+
let result = [];
116+
Object.keys(projection).forEach(k => {
117+
if (isObject(projection[k])) {
118+
// $FlowFixMe
119+
const tmp = toDottedList(projection[k], prev ? [ ...prev, k] : [k]);
120+
if (Array.isArray(tmp)) {
121+
result = result.concat(tmp);
122+
return;
123+
}
124+
}
125+
126+
if (prev) {
127+
result.push([...prev, k].join('.'));
128+
} else {
129+
result.push(k);
130+
}
131+
});
132+
return result.length > 0 ? result : true;
133+
}

src/types/SearchHitItem.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/* @flow */
2+
3+
import { TypeComposer } from 'graphql-compose';
4+
import { getTypeName, getOrSetType, desc } from '../utils';
5+
import type { SearchOptsT } from './SearchOutput';
6+
7+
export function getSearchHitItemTC(opts: SearchOptsT = {}): TypeComposer {
8+
const name = getTypeName('SearchHitItem', opts);
9+
10+
return getOrSetType(name, () =>
11+
// $FlowFixMe
12+
TypeComposer.create({
13+
name,
14+
fields: {
15+
_index: 'String',
16+
_type: 'String',
17+
_id: 'String',
18+
_score: 'Float',
19+
_source: opts.sourceTC || 'JSON',
20+
21+
// if arg.explain = true
22+
_shard: {
23+
type: 'String',
24+
description: desc(`Use explain API on query`),
25+
},
26+
_node: {
27+
type: 'String',
28+
description: desc(`Use explain API on query`),
29+
},
30+
_explanation: {
31+
type: 'JSON',
32+
description: desc(`Use explain API on query`),
33+
},
34+
35+
// if arg.version = true
36+
_version: 'Int',
37+
38+
// if args.highlight is provided
39+
highlight: 'JSON',
40+
41+
// return sort values for search_after
42+
sort: 'JSON',
43+
},
44+
}));
45+
}

src/types/SearchOutput.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* @flow */
2+
3+
import { TypeComposer } from 'graphql-compose';
4+
import { getTypeName, getOrSetType, desc } from '../utils';
5+
import type { FieldsMapByElasticType } from '../mappingConverter';
6+
import getShardsTC from './Shards';
7+
import { getSearchHitItemTC } from './SearchHitItem';
8+
9+
export type SearchOptsT = {
10+
prefix?: string,
11+
postfix?: string,
12+
fieldMap?: FieldsMapByElasticType,
13+
sourceTC?: TypeComposer,
14+
};
15+
16+
export function getSearchOutputTC(opts: SearchOptsT = {}): TypeComposer {
17+
const name = getTypeName('SearchOutput', opts);
18+
const nameHits = getTypeName('SearchHits', opts);
19+
20+
return getOrSetType(name, () =>
21+
// $FlowFixMe
22+
TypeComposer.create({
23+
name,
24+
fields: {
25+
took: 'Int',
26+
timed_out: 'Boolean',
27+
_shards: getShardsTC(opts),
28+
hits: getOrSetType(nameHits, () =>
29+
// $FlowFixMe
30+
TypeComposer.create({
31+
name: nameHits,
32+
fields: {
33+
total: 'Int',
34+
max_score: 'Float',
35+
hits: [getSearchHitItemTC(opts)],
36+
},
37+
})),
38+
},
39+
}));
40+
}

src/types/Shards.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { TypeComposer } from 'graphql-compose';
2+
import { getTypeName, getOrSetType } from '../utils';
3+
4+
export default function getShardsTC(opts: mixed = {}): TypeComposer {
5+
const name = getTypeName('MetaShards', opts);
6+
7+
return getOrSetType(name, () =>
8+
// $FlowFixMe
9+
TypeComposer.create({
10+
name,
11+
fields: {
12+
total: 'Int',
13+
successful: 'Int',
14+
failed: 'Int',
15+
},
16+
}));
17+
}

0 commit comments

Comments
 (0)