From 6aedd157b6dc18a140091e26aad621c04167650e Mon Sep 17 00:00:00 2001 From: frankast Date: Mon, 2 Apr 2018 21:44:18 +0600 Subject: [PATCH 1/7] feat: add resolver 'findById' --- src/resolvers/findById.js | 172 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/resolvers/findById.js diff --git a/src/resolvers/findById.js b/src/resolvers/findById.js new file mode 100644 index 0000000..09da8b7 --- /dev/null +++ b/src/resolvers/findById.js @@ -0,0 +1,172 @@ +/* @flow */ +/* eslint-disable no-param-reassign */ + +import { Resolver, TypeComposer, InputTypeComposer, isObject } from 'graphql-compose'; +import type { ResolveParams, ProjectionType } from 'graphql-compose'; +import type { FieldsMapByElasticType } from '../mappingConverter'; +import ElasticApiParser from '../ElasticApiParser'; +import { getSearchBodyITC, prepareBodyInResolve } from '../elasticDSL/SearchBody'; +import { getSearchOutputTC } from '../types/SearchOutput'; + +export type ElasticSearchResolverOpts = { + prefix?: ?string, + elasticIndex: string, + elasticType: string, + elasticClient: Object, +}; + +export default function createFindByIdResolver( + fieldMap: FieldsMapByElasticType, + sourceTC: TypeComposer, + opts: ElasticSearchResolverOpts +): Resolver { + if (!fieldMap || !fieldMap._all) { + throw new Error( + 'First arg for Resolver findById() should be fieldMap of FieldsMapByElasticType type.' + ); + } + + if (!sourceTC || sourceTC.constructor.name !== 'TypeComposer') { + throw new Error('Second arg for Resolver findById() should be instance of TypeComposer.'); + } + + const prefix = opts.prefix || 'Es'; + + const parser = new ElasticApiParser({ + elasticClient: opts.elasticClient, + prefix, + }); + + const searchITC = getSearchBodyITC({ prefix, fieldMap }).removeField([ + 'size', + 'from', + '_source', + 'explain', + 'version', + ]); + + const findByIdFC = parser.generateFieldConfig('findById', { + index: opts.elasticIndex, + type: opts.elasticType, + }); + + const argsConfigMap = Object.assign({}, findByIdFC.args, { + body: { + type: searchITC.getType(), + }, + }); + + delete argsConfigMap.index; // index can not be changed, it hardcoded in findByIdFC + delete argsConfigMap.type; // type can not be changed, it hardcoded in findByIdFC + delete argsConfigMap.explain; // added automatically if requested _shard, _node, _explanation + delete argsConfigMap.version; // added automatically if requested _version + delete argsConfigMap._source; // added automatically due projection + delete argsConfigMap._sourceExclude; // added automatically due projection + delete argsConfigMap._sourceInclude; // added automatically due projection + delete argsConfigMap.trackScores; // added automatically due projection (is _scrore requested with sort) + + delete argsConfigMap.size; + delete argsConfigMap.from; + + argsConfigMap._id = 'String'; // id of record in index + + const topLevelArgs = ['_id']; + + argsConfigMap.opts = InputTypeComposer.create({ + name: `${sourceTC.getTypeName()}Opts`, + fields: Object.assign({}, argsConfigMap), + }).removeField(topLevelArgs); + + Object.keys(argsConfigMap).forEach(argKey => { + if (topLevelArgs.indexOf(argKey) === -1) { + delete argsConfigMap[argKey]; + } + }); + + const type = getSearchOutputTC({ prefix, fieldMap, sourceTC }); + let hitsType; + try { + hitsType = type.get('hits.hits'); + } catch (e) { + hitsType = 'JSON'; + } + type + .addFields({ + count: 'Int', + max_score: 'Float', + hits: hitsType ? [hitsType] : 'JSON', + }) + .reorderFields(['hits', 'count', 'aggregations', 'max_score', 'took', 'timed_out', '_shards']); + + return new Resolver({ + type, + name: 'findById', + kind: 'query', + args: argsConfigMap, + resolve: async (rp: ResolveParams<*, *>) => { + const args: Object = rp.args || {}; + const projection = rp.projection || {}; + if (!args.body) args.body = {}; + + const { hits = {} } = projection; + + if (hits && typeof hits === 'object') { + // Turn on explain if in projection requested this fields: + if (hits._shard || hits._node || hits._explanation) { + args.body.explain = true; + } + + if (hits._version) { + args.body.version = true; + } + + if (!hits._source) { + args.body._source = false; + } else { + args.body._source = toDottedList(hits._source); + } + + if (hits._score) { + args.body.track_scores = true; + } + } + + if (args._id) { + args.body._id = args._id; + delete args._id; + } + + if (args.body) { + args.body = prepareBodyInResolve(args.body, fieldMap); + } + + const res: any = await findByIdFC.resolve(rp.source, args, rp.context, rp.info); + + res.count = res.hits.total; + res.max_score = res.hits.max_score; + res.hits = res.hits.hits; + + return res; + }, + }); +} + +export function toDottedList(projection: ProjectionType, prev?: string[]): string[] | boolean { + let result = []; + Object.keys(projection).forEach(k => { + if (isObject(projection[k])) { + const tmp = toDottedList(projection[k], prev ? [...prev, k] : [k]); + if (Array.isArray(tmp)) { + result = result.concat(tmp); + return; + } + } + + if (prev) { + result.push([...prev, k].join('.')); + } else { + result.push(k); + } + }); + return result.length > 0 ? result : true; +} From dd6311dcce49c799767c577bded3503d61b21733 Mon Sep 17 00:00:00 2001 From: Borodayev Valeriy Date: Tue, 3 Apr 2018 16:43:02 +0600 Subject: [PATCH 2/7] wip(findById): refactor resolver, add test --- src/ElasticApiParser.js | 3 +- src/resolvers/__tests__/findById-test.js | 34 +++++++++++ src/resolvers/findById.js | 72 +++++------------------- 3 files changed, 49 insertions(+), 60 deletions(-) create mode 100644 src/resolvers/__tests__/findById-test.js diff --git a/src/ElasticApiParser.js b/src/ElasticApiParser.js index ecce7a9..87a3923 100644 --- a/src/ElasticApiParser.js +++ b/src/ElasticApiParser.js @@ -75,7 +75,7 @@ export default class ElasticApiParser { parsedSource: ElasticParsedSourceT; constructor(opts: ElasticApiParserOptsT = {}) { - // avaliable varsions can be found in installed package `elasticsearch` + // avaliable versions can be found in installed package `elasticsearch` // in file /node_modules/elasticsearch/src/lib/apis/index.js this.apiVersion = opts.apiVersion || @@ -312,7 +312,6 @@ export default class ElasticApiParser { ...args, }); } - return client[elasticMethod]({ ...methodArgs, ...args }); }, }; diff --git a/src/resolvers/__tests__/findById-test.js b/src/resolvers/__tests__/findById-test.js new file mode 100644 index 0000000..5b71414 --- /dev/null +++ b/src/resolvers/__tests__/findById-test.js @@ -0,0 +1,34 @@ +/* @flow */ + +import { Resolver } from 'graphql-compose'; +import createFindByIdResolver, * as FindById from '../findById'; +import elasticClient from '../../__mocks__/elasticClient'; +import { CvTC, CvFieldMap } from '../../__mocks__/cv'; + +describe.only('findById', () => { + it('return instance of Resolver', () => { + expect(createFindByIdResolver(CvFieldMap, CvTC, elasticClient)).toBeInstanceOf(Resolver); + }); + + it('return result', () => { + const findByIdResolver = createFindByIdResolver(CvFieldMap, CvTC, { + elasticClient, + elasticIndex: 'cv', + elasticType: 'cv', + }); + return findByIdResolver + .resolve({ args: { id: '4554' }, context: { elasticClient } }) + .then(res => { + console.log(res); // eslint-disable-line + }); + }); + + it('toDottedList()', () => { + expect(FindById.toDottedList({ a: { b: true, c: { e: true } }, d: true })).toEqual([ + 'a.b', + 'a.c.e', + 'd', + ]); + expect(FindById.toDottedList({})).toEqual(true); + }); +}); diff --git a/src/resolvers/findById.js b/src/resolvers/findById.js index 09da8b7..253f7d0 100644 --- a/src/resolvers/findById.js +++ b/src/resolvers/findById.js @@ -1,14 +1,13 @@ /* @flow */ /* eslint-disable no-param-reassign */ -import { Resolver, TypeComposer, InputTypeComposer, isObject } from 'graphql-compose'; +import { Resolver, TypeComposer, isObject } from 'graphql-compose'; import type { ResolveParams, ProjectionType } from 'graphql-compose'; import type { FieldsMapByElasticType } from '../mappingConverter'; import ElasticApiParser from '../ElasticApiParser'; -import { getSearchBodyITC, prepareBodyInResolve } from '../elasticDSL/SearchBody'; import { getSearchOutputTC } from '../types/SearchOutput'; -export type ElasticSearchResolverOpts = { +export type ElasticFindByIdResolverOpts = { prefix?: ?string, elasticIndex: string, elasticType: string, @@ -18,7 +17,7 @@ export type ElasticSearchResolverOpts = { export default function createFindByIdResolver( fieldMap: FieldsMapByElasticType, sourceTC: TypeComposer, - opts: ElasticSearchResolverOpts + opts: ElasticFindByIdResolverOpts ): Resolver { if (!fieldMap || !fieldMap._all) { throw new Error( @@ -37,45 +36,14 @@ export default function createFindByIdResolver( prefix, }); - const searchITC = getSearchBodyITC({ prefix, fieldMap }).removeField([ - 'size', - 'from', - '_source', - 'explain', - 'version', - ]); - - const findByIdFC = parser.generateFieldConfig('findById', { + const findByIdFC = parser.generateFieldConfig('getSource', { index: opts.elasticIndex, type: opts.elasticType, }); - const argsConfigMap = Object.assign({}, findByIdFC.args, { - body: { - type: searchITC.getType(), - }, - }); - - delete argsConfigMap.index; // index can not be changed, it hardcoded in findByIdFC - delete argsConfigMap.type; // type can not be changed, it hardcoded in findByIdFC - delete argsConfigMap.explain; // added automatically if requested _shard, _node, _explanation - delete argsConfigMap.version; // added automatically if requested _version - delete argsConfigMap._source; // added automatically due projection - delete argsConfigMap._sourceExclude; // added automatically due projection - delete argsConfigMap._sourceInclude; // added automatically due projection - delete argsConfigMap.trackScores; // added automatically due projection (is _scrore requested with sort) - - delete argsConfigMap.size; - delete argsConfigMap.from; - - argsConfigMap._id = 'String'; // id of record in index - - const topLevelArgs = ['_id']; + const argsConfigMap = Object.assign({}, findByIdFC.args); - argsConfigMap.opts = InputTypeComposer.create({ - name: `${sourceTC.getTypeName()}Opts`, - fields: Object.assign({}, argsConfigMap), - }).removeField(topLevelArgs); + const topLevelArgs = ['id']; Object.keys(argsConfigMap).forEach(argKey => { if (topLevelArgs.indexOf(argKey) === -1) { @@ -106,38 +74,26 @@ export default function createFindByIdResolver( resolve: async (rp: ResolveParams<*, *>) => { const args: Object = rp.args || {}; const projection = rp.projection || {}; - if (!args.body) args.body = {}; - const { hits = {} } = projection; if (hits && typeof hits === 'object') { - // Turn on explain if in projection requested this fields: - if (hits._shard || hits._node || hits._explanation) { - args.body.explain = true; - } - if (hits._version) { - args.body.version = true; + args.version = true; } if (!hits._source) { - args.body._source = false; + args._source = false; } else { - args.body._source = toDottedList(hits._source); + args._source = toDottedList(hits._source); } - if (hits._score) { - args.body.track_scores = true; + if (hits._realtime) { + args.realtime = true; } - } - if (args._id) { - args.body._id = args._id; - delete args._id; - } - - if (args.body) { - args.body = prepareBodyInResolve(args.body, fieldMap); + if (hits._refresh) { + args.refresh = true; + } } const res: any = await findByIdFC.resolve(rp.source, args, rp.context, rp.info); From d053c17f614f5c78e2688b044769821313be377a Mon Sep 17 00:00:00 2001 From: frankast Date: Sun, 8 Apr 2018 15:03:39 +0600 Subject: [PATCH 3/7] wip: refactor tests, add findByIdR in 'composeWithElastic' --- src/composeWithElastic.js | 3 +++ src/resolvers/__tests__/findById-test.js | 27 +++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/composeWithElastic.js b/src/composeWithElastic.js index d32856e..251ec5e 100644 --- a/src/composeWithElastic.js +++ b/src/composeWithElastic.js @@ -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 createFindByIdResolver from './resolvers/findById'; import type { ElasticMappingT } from './mappingConverter'; @@ -63,9 +64,11 @@ export function composeWithElastic(opts: composeWithElasticOptsT): TypeComposer const searchR = createSearchResolver(fieldMap, sourceTC, opts); const searchConnectionR = createSearchConnectionResolver(searchR, opts); + const findByIdR = createFindByIdResolver(fieldMap, sourceTC, opts); sourceTC.addResolver(searchR); sourceTC.addResolver(searchConnectionR); + sourceTC.addResolver(findByIdR); return sourceTC; } diff --git a/src/resolvers/__tests__/findById-test.js b/src/resolvers/__tests__/findById-test.js index 5b71414..da7e29b 100644 --- a/src/resolvers/__tests__/findById-test.js +++ b/src/resolvers/__tests__/findById-test.js @@ -5,22 +5,25 @@ import createFindByIdResolver, * as FindById from '../findById'; import elasticClient from '../../__mocks__/elasticClient'; import { CvTC, CvFieldMap } from '../../__mocks__/cv'; -describe.only('findById', () => { +const findByIdResolver = createFindByIdResolver(CvFieldMap, CvTC, { + elasticClient, + elasticIndex: 'cv', + elasticType: 'cv', +}); + +describe('findById', () => { it('return instance of Resolver', () => { - expect(createFindByIdResolver(CvFieldMap, CvTC, elasticClient)).toBeInstanceOf(Resolver); + expect(findByIdResolver).toBeInstanceOf(Resolver); }); - it('return result', () => { - const findByIdResolver = createFindByIdResolver(CvFieldMap, CvTC, { - elasticClient, - elasticIndex: 'cv', - elasticType: 'cv', - }); - return findByIdResolver - .resolve({ args: { id: '4554' }, context: { elasticClient } }) - .then(res => { + it('check args', () => { + expect(findByIdResolver.hasArg('id')).toBeTruthy(); + }); + + it('resolve', () => { + findByIdResolver.resolve({ args: { id: '4554' }, context: { elasticClient } }).then(res => { console.log(res); // eslint-disable-line - }); + }); }); it('toDottedList()', () => { From 4945dd9d36b002e935a8cac76333c67385d22c01 Mon Sep 17 00:00:00 2001 From: frankast Date: Wed, 11 Apr 2018 18:56:23 +0600 Subject: [PATCH 4/7] wip: add FindByIdOutput type, try to return result --- __fixtures__/index.js | 27 ++++ __fixtures__/schemaElastic/GovStatBin.js | 90 ++++++++++++ .../schemaElastic/elasticAwsConnection.js | 131 ++++++++++++++++++ __fixtures__/schemaElastic/elasticClient.js | 33 +++++ package.json | 4 +- src/resolvers/findById.js | 61 ++------ src/resolvers/search.js | 2 + src/types/FindByIdOutput.js | 30 ++++ yarn.lock | 85 +++++++++++- 9 files changed, 411 insertions(+), 52 deletions(-) create mode 100644 __fixtures__/index.js create mode 100644 __fixtures__/schemaElastic/GovStatBin.js create mode 100644 __fixtures__/schemaElastic/elasticAwsConnection.js create mode 100644 __fixtures__/schemaElastic/elasticClient.js create mode 100644 src/types/FindByIdOutput.js diff --git a/__fixtures__/index.js b/__fixtures__/index.js new file mode 100644 index 0000000..04bc78b --- /dev/null +++ b/__fixtures__/index.js @@ -0,0 +1,27 @@ +// @flow + +import express from 'express'; +import graphqlHttp from 'express-graphql'; +import { GQC } from 'graphql-compose'; + +import { GovStatBinEsTC } from './schemaElastic/GovStatBin'; + +GQC.rootQuery().addFields({ + userById: GovStatBinEsTC.getResolver('findById'), + userSearch: GovStatBinEsTC.getResolver('search'), +}); +const schema = GQC.buildSchema(); + +const app = express(); + +app.use( + '/', + graphqlHttp(() => { + return { + schema, + graphiql: true, + }; + }) +); + +app.listen(8090, console.log(`App works on 8090...`)); diff --git a/__fixtures__/schemaElastic/GovStatBin.js b/__fixtures__/schemaElastic/GovStatBin.js new file mode 100644 index 0000000..f89d492 --- /dev/null +++ b/__fixtures__/schemaElastic/GovStatBin.js @@ -0,0 +1,90 @@ +// @flow +/* eslint-disable no-param-reassign */ + +import { composeWithElastic } from '../../src/index'; +import elasticClient from './elasticClient'; + +export const govStatBinMapping = { + name: { type: 'keyword', boost: 5 }, + nameKZ: { type: 'keyword', boost: 5 }, + regAt: { type: 'date' }, + code1: { type: 'double' }, + code2: { type: 'keyword' }, + act: { type: 'text' }, + crp: { type: 'double' }, + size: { type: 'keyword' }, + cato: { type: 'double' }, + loc: { type: 'keyword' }, + addr: { type: 'keyword' }, + chef: { type: 'keyword' }, + id_keyword: { type: 'keyword', boost: 10 }, + shortName: { type: 'keyword', boost: 7 }, +}; + +export const GovStatBinEsTC = composeWithElastic({ + graphqlTypeName: 'GovStatBinEsTC', + elasticIndex: 'bin', + elasticType: 'bin', + elasticMapping: { + properties: govStatBinMapping, + }, + elasticClient, +}); + +export function getQuery(filter: Object = {}) { + if (!filter) return null; + + const must = []; + + if (filter.q) { + must.push({ + function_score: { + boost_mode: 'multiply', + query: { + bool: { + should: [ + { + multi_match: { + query: filter.q, + fields: ['shortName^70', 'name^50', 'nameKZ^40', '_all'], + type: 'phrase', // or keyword + slop: 100, + boost: 10, + }, + }, + { + multi_match: { + query: filter.q, + fields: ['name^50', '_all'], + operator: 'and', + }, + }, + ], + }, + }, + }, + }); + } + + if (must.length > 0) { + return { bool: { must } }; + } + return null; +} + +GovStatBinEsTC.wrapResolver('searchConnection', resolver => { + resolver.addArgs({ + filter: 'JSON', + withAggs: 'Boolean', + }); + + return resolver.wrapResolve(next => rp => { + if (rp.args) { + if (rp.args.filter) { + rp.args.query = getQuery(rp.args.filter); + delete rp.args.filter; + } + } + return next(rp); + }); +}); diff --git a/__fixtures__/schemaElastic/elasticAwsConnection.js b/__fixtures__/schemaElastic/elasticAwsConnection.js new file mode 100644 index 0000000..5941fdb --- /dev/null +++ b/__fixtures__/schemaElastic/elasticAwsConnection.js @@ -0,0 +1,131 @@ +/* @flow */ +/* eslint-disable no-restricted-syntax, guard-for-in, func-names */ + +/** + * A Connection handler for Amazon ES. + * + * Uses the aws-sdk to make signed requests to an Amazon ES endpoint. + * Define the Amazon ES config and the connection handler + * in the client configuration: + * + * var es = require('elasticsearch').Client({ + * hosts: 'https://amazon-es-host.us-east-1.es.amazonaws.com', + * connectionClass: require('http-aws-es'), + * amazonES: { + * region: 'us-east-1', + * accessKey: 'AKID', + * secretKey: 'secret', + * credentials: new AWS.EnvironmentCredentials('AWS') // Optional + * } + * }); + * + * @param client {Client} - The Client that this class belongs to + * @param config {Object} - Configuration options + * @param [config.protocol=http:] {String} - The HTTP protocol that this connection will use, can be set to https: + * @class HttpConnector + */ +import AWS from 'aws-sdk'; +import HttpConnector from 'elasticsearch/src/lib/connectors/http'; +import _ from 'elasticsearch/src/lib/utils'; +import zlib from 'zlib'; + +export default class HttpAmazonESConnector extends HttpConnector { + constructor(host: Object, config: Object) { + super(host, config); + this.endpoint = new AWS.Endpoint(host.host); + const c = config.amazonES; + if (c.credentials) { + this.creds = c.credentials; + } else { + this.creds = new AWS.Credentials(c.accessKey, c.secretKey); + } + this.amazonES = c; + } + + request(params: Object, cb: Function) { + let incoming; + let timeoutId; + let req; + let status = 0; + let headers = {}; + const log = this.log; + let response; + + const reqParams = this.makeReqParams(params); + // general clean-up procedure to run after the request + // completes, has an error, or is aborted. + const cleanUp = _.bind(err => { + clearTimeout(timeoutId); + + if (req) { + req.removeAllListeners(); + } + + if (incoming) { + incoming.removeAllListeners(); + } + + if (err instanceof Error === false) { + err = void 0; // eslint-disable-line + } + + log.trace(params.method, reqParams, params.body, response, status); + if (err) { + cb(err); + } else { + cb(err, response, status, headers); + } + }, this); + + const request = new AWS.HttpRequest(this.endpoint); + + // copy across params + for (const p in reqParams) { + request[p] = reqParams[p]; + } + request.region = this.amazonES.region; + if (params.body) request.body = params.body; + if (!request.headers) request.headers = {}; + request.headers['presigned-expires'] = false; + request.headers.Host = this.endpoint.host; + + // Sign the request (Sigv4) + const signer = new AWS.Signers.V4(request, 'es'); + signer.addAuthorization(this.creds, new Date()); + + const send = new AWS.NodeHttpClient(); + req = send.handleRequest( + request, + null, + _incoming => { + incoming = _incoming; + status = incoming.statusCode; + headers = incoming.headers; + response = ''; + + const encoding = (headers['content-encoding'] || '').toLowerCase(); + if (encoding === 'gzip' || encoding === 'deflate') { + incoming = incoming.pipe(zlib.createUnzip()); + } + + incoming.setEncoding('utf8'); + incoming.on('data', d => { + response += d; + }); + + incoming.on('error', cleanUp); + incoming.on('end', cleanUp); + }, + cleanUp + ); + + req.on('error', cleanUp); + + req.setNoDelay(true); + req.setSocketKeepAlive(true); + + return function() { + req.abort(); + }; + } +} diff --git a/__fixtures__/schemaElastic/elasticClient.js b/__fixtures__/schemaElastic/elasticClient.js new file mode 100644 index 0000000..eb3c56d --- /dev/null +++ b/__fixtures__/schemaElastic/elasticClient.js @@ -0,0 +1,33 @@ +// @flow + +import elasticsearch from 'elasticsearch'; +import awsElasticConnection from './elasticAwsConnection'; + +// TODO: make test and production connections depends of NODE_ENV +const ELASTIC_HOST = `https://search-rabota-staging-vsl4lltip4tl46tzb556vc23uq.eu-west-1.es.amazonaws.com`; +const elasticClient = new elasticsearch.Client({ + host: ELASTIC_HOST, + connectionClass: awsElasticConnection, + amazonES: { + region: /([^.]+).es.amazonaws.com/.exec(ELASTIC_HOST)[1], + accessKey: 'AKIAI36RLINJS36JW7JQ', + secretKey: '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', + }, + apiVersion: '5.0', + // log: 'trace', +}); + +// elasticClient.ping( +// { +// requestTimeout: 30000, +// }, +// error => { +// if (error) { +// console.error(' elasticsearch cluster is down'); // eslint-disable-line no-console +// } else { +// console.log(' connected to elasticsearch'); // eslint-disable-line no-console +// } +// } +// ); + +export default elasticClient; diff --git a/package.json b/package.json index 163139e..7770d55 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "graphql-compose": ">=2.13.1 || >=3.0.0" }, "devDependencies": { + "aws-sdk": "^2.224.1", "babel-cli": "^6.26.0", "babel-eslint": "^8.2.1", "babel-jest": "^22.2.2", @@ -85,6 +86,7 @@ "docker:v2": "node ./scripts/docker/start 2 & wait", "docker:v5": "node ./scripts/docker/start 5 & wait", "link": "yarn build && yarn link graphql-compose && yarn link", - "unlink": "yarn unlink graphql-compose && yarn add graphql-compose" + "unlink": "yarn unlink graphql-compose && yarn add graphql-compose", + "my-demo": "nodemon --exec babel-node ./__fixtures__/index.js" } } diff --git a/src/resolvers/findById.js b/src/resolvers/findById.js index 253f7d0..da0b1f9 100644 --- a/src/resolvers/findById.js +++ b/src/resolvers/findById.js @@ -5,7 +5,7 @@ import { Resolver, TypeComposer, isObject } from 'graphql-compose'; import type { ResolveParams, ProjectionType } from 'graphql-compose'; import type { FieldsMapByElasticType } from '../mappingConverter'; import ElasticApiParser from '../ElasticApiParser'; -import { getSearchOutputTC } from '../types/SearchOutput'; +import { getFindByIdOutputTC } from '../types/FindByIdOutput'; export type ElasticFindByIdResolverOpts = { prefix?: ?string, @@ -39,6 +39,7 @@ export default function createFindByIdResolver( const findByIdFC = parser.generateFieldConfig('getSource', { index: opts.elasticIndex, type: opts.elasticType, + _source: true, }); const argsConfigMap = Object.assign({}, findByIdFC.args); @@ -51,20 +52,7 @@ export default function createFindByIdResolver( } }); - const type = getSearchOutputTC({ prefix, fieldMap, sourceTC }); - let hitsType; - try { - hitsType = type.get('hits.hits'); - } catch (e) { - hitsType = 'JSON'; - } - type - .addFields({ - count: 'Int', - max_score: 'Float', - hits: hitsType ? [hitsType] : 'JSON', - }) - .reorderFields(['hits', 'count', 'aggregations', 'max_score', 'took', 'timed_out', '_shards']); + const type = getFindByIdOutputTC({ prefix, fieldMap, sourceTC }); return new Resolver({ type, @@ -72,37 +60,18 @@ export default function createFindByIdResolver( kind: 'query', args: argsConfigMap, resolve: async (rp: ResolveParams<*, *>) => { - const args: Object = rp.args || {}; - const projection = rp.projection || {}; - const { hits = {} } = projection; - - if (hits && typeof hits === 'object') { - if (hits._version) { - args.version = true; - } - - if (!hits._source) { - args._source = false; - } else { - args._source = toDottedList(hits._source); - } - - if (hits._realtime) { - args.realtime = true; - } - - if (hits._refresh) { - args.refresh = true; - } - } - - const res: any = await findByIdFC.resolve(rp.source, args, rp.context, rp.info); - - res.count = res.hits.total; - res.max_score = res.hits.max_score; - res.hits = res.hits.hits; - - return res; + // const projection = rp.projection || {}; + const res = await findByIdFC.resolve(rp.source, rp.args, rp.context, rp.info); + console.log(res); + + return { + _index: opts.elasticIndex, + _type: opts.elasticType, + _id: rp.args.id, + _version: 1, + found: !!res, + _source: res, + }; }, }); } diff --git a/src/resolvers/search.js b/src/resolvers/search.js index 3426981..75dcae6 100644 --- a/src/resolvers/search.js +++ b/src/resolvers/search.js @@ -180,6 +180,8 @@ export default function createSearchResolver( const res: any = await searchFC.resolve(rp.source, args, rp.context, rp.info); + console.log(res); + res.count = res.hits.total; res.max_score = res.hits.max_score; res.hits = res.hits.hits; diff --git a/src/types/FindByIdOutput.js b/src/types/FindByIdOutput.js new file mode 100644 index 0000000..c78b4ae --- /dev/null +++ b/src/types/FindByIdOutput.js @@ -0,0 +1,30 @@ +/* @flow */ + +import { TypeComposer } from 'graphql-compose'; +import { getTypeName, getOrSetType } from '../utils'; +import type { FieldsMapByElasticType } from '../mappingConverter'; + +export type SearchOptsT = { + prefix?: string, + postfix?: string, + fieldMap?: FieldsMapByElasticType, + sourceTC?: TypeComposer, +}; + +export function getFindByIdOutputTC(opts: SearchOptsT = {}): TypeComposer { + const name = getTypeName('FindByIdOutput', opts); + + return getOrSetType(name, () => + TypeComposer.create({ + name, + fields: { + _index: 'String', + _type: 'String', + _id: 'String', + _version: 'Int', + _found: 'Boolean', + _source: opts.sourceTC || 'JSON', + }, + }) + ); +} diff --git a/yarn.lock b/yarn.lock index 4b11fe4..4de91dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -437,6 +437,21 @@ atob@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d" +aws-sdk@^2.224.1: + version "2.224.1" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.224.1.tgz#82fe93e10b3e818f315c35ce8667cdc8db94a0b3" + dependencies: + buffer "4.9.1" + events "1.1.1" + ieee754 "1.1.8" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.1.0" + xml2js "0.4.17" + xmlbuilder "4.2.1" + aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" @@ -1091,6 +1106,10 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +base64-js@^1.0.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.3.tgz#fb13668233d9614cf5fb4bce95a9ba4096cdf801" + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -1226,6 +1245,14 @@ buffer-shims@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" +buffer@4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -2147,6 +2174,10 @@ event-stream@~3.3.0: stream-combiner "~0.0.4" through "~2.3.1" +events@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + exec-sh@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.0.tgz#14f75de3f20d286ef933099b2ce50a90359cef10" @@ -2919,6 +2950,14 @@ iconv-lite@^0.4.17: version "0.4.18" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" +ieee754@1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +ieee754@^1.1.4: + version "1.1.11" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455" + ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" @@ -3650,6 +3689,10 @@ jest@^22.3.0: import-local "^1.0.0" jest-cli "^22.3.0" +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" @@ -4796,6 +4839,10 @@ pstree.remy@^1.1.0: dependencies: ps-tree "^1.1.0" +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -4828,6 +4875,10 @@ query-string@^5.0.1: object-assign "^4.1.0" strict-uri-encode "^1.0.0" +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + randomatic@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" @@ -5299,7 +5350,11 @@ sane@^2.0.0: optionalDependencies: fsevents "^1.1.1" -sax@^1.2.4: +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + +sax@>=0.6.0, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -6054,6 +6109,13 @@ url-template@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + use@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8" @@ -6081,14 +6143,14 @@ utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" +uuid@3.1.0, uuid@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + uuid@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" -uuid@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" - v8flags@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" @@ -6249,6 +6311,19 @@ xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" +xml2js@0.4.17: + version "0.4.17" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.17.tgz#17be93eaae3f3b779359c795b419705a8817e868" + dependencies: + sax ">=0.6.0" + xmlbuilder "^4.1.0" + +xmlbuilder@4.2.1, xmlbuilder@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.2.1.tgz#aa58a3041a066f90eaa16c2f5389ff19f3f461a5" + dependencies: + lodash "^4.0.0" + xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" From db1eb1b88dad8ea66a3064efd15963fbf657378b Mon Sep 17 00:00:00 2001 From: frankast Date: Wed, 11 Apr 2018 19:50:50 +0600 Subject: [PATCH 5/7] wip --- __fixtures__/schemaElastic/elasticClient.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/__fixtures__/schemaElastic/elasticClient.js b/__fixtures__/schemaElastic/elasticClient.js index eb3c56d..7a378ff 100644 --- a/__fixtures__/schemaElastic/elasticClient.js +++ b/__fixtures__/schemaElastic/elasticClient.js @@ -3,15 +3,14 @@ import elasticsearch from 'elasticsearch'; import awsElasticConnection from './elasticAwsConnection'; -// TODO: make test and production connections depends of NODE_ENV -const ELASTIC_HOST = `https://search-rabota-staging-vsl4lltip4tl46tzb556vc23uq.eu-west-1.es.amazonaws.com`; +const ELASTIC_HOST = ``; const elasticClient = new elasticsearch.Client({ host: ELASTIC_HOST, connectionClass: awsElasticConnection, amazonES: { region: /([^.]+).es.amazonaws.com/.exec(ELASTIC_HOST)[1], - accessKey: 'AKIAI36RLINJS36JW7JQ', - secretKey: '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', + accessKey: '', + secretKey: '', }, apiVersion: '5.0', // log: 'trace', From ae21a614b944c030f38bc0b5bd21a656a48cff2f Mon Sep 17 00:00:00 2001 From: Borodayev Valeriy Date: Thu, 12 Apr 2018 15:31:41 +0600 Subject: [PATCH 6/7] feat(Resolvers): add 'findById' & 'updateById' resolvers --- __fixtures__/index.js | 27 ---- __fixtures__/schemaElastic/GovStatBin.js | 90 ------------ .../schemaElastic/elasticAwsConnection.js | 131 ------------------ __fixtures__/schemaElastic/elasticClient.js | 32 ----- package.json | 3 +- src/composeWithElastic.js | 3 + src/resolvers/__tests__/findById-test.js | 11 +- src/resolvers/findById.js | 54 +++----- src/resolvers/search.js | 6 +- src/resolvers/updateById.js | 111 +++++++++++++++ src/types/FindByIdOutput.js | 13 +- src/types/UpdateByIdOutput.js | 30 ++++ 12 files changed, 172 insertions(+), 339 deletions(-) delete mode 100644 __fixtures__/index.js delete mode 100644 __fixtures__/schemaElastic/GovStatBin.js delete mode 100644 __fixtures__/schemaElastic/elasticAwsConnection.js delete mode 100644 __fixtures__/schemaElastic/elasticClient.js create mode 100644 src/resolvers/updateById.js create mode 100644 src/types/UpdateByIdOutput.js diff --git a/__fixtures__/index.js b/__fixtures__/index.js deleted file mode 100644 index 04bc78b..0000000 --- a/__fixtures__/index.js +++ /dev/null @@ -1,27 +0,0 @@ -// @flow - -import express from 'express'; -import graphqlHttp from 'express-graphql'; -import { GQC } from 'graphql-compose'; - -import { GovStatBinEsTC } from './schemaElastic/GovStatBin'; - -GQC.rootQuery().addFields({ - userById: GovStatBinEsTC.getResolver('findById'), - userSearch: GovStatBinEsTC.getResolver('search'), -}); -const schema = GQC.buildSchema(); - -const app = express(); - -app.use( - '/', - graphqlHttp(() => { - return { - schema, - graphiql: true, - }; - }) -); - -app.listen(8090, console.log(`App works on 8090...`)); diff --git a/__fixtures__/schemaElastic/GovStatBin.js b/__fixtures__/schemaElastic/GovStatBin.js deleted file mode 100644 index f89d492..0000000 --- a/__fixtures__/schemaElastic/GovStatBin.js +++ /dev/null @@ -1,90 +0,0 @@ -// @flow -/* eslint-disable no-param-reassign */ - -import { composeWithElastic } from '../../src/index'; -import elasticClient from './elasticClient'; - -export const govStatBinMapping = { - name: { type: 'keyword', boost: 5 }, - nameKZ: { type: 'keyword', boost: 5 }, - regAt: { type: 'date' }, - code1: { type: 'double' }, - code2: { type: 'keyword' }, - act: { type: 'text' }, - crp: { type: 'double' }, - size: { type: 'keyword' }, - cato: { type: 'double' }, - loc: { type: 'keyword' }, - addr: { type: 'keyword' }, - chef: { type: 'keyword' }, - id_keyword: { type: 'keyword', boost: 10 }, - shortName: { type: 'keyword', boost: 7 }, -}; - -export const GovStatBinEsTC = composeWithElastic({ - graphqlTypeName: 'GovStatBinEsTC', - elasticIndex: 'bin', - elasticType: 'bin', - elasticMapping: { - properties: govStatBinMapping, - }, - elasticClient, -}); - -export function getQuery(filter: Object = {}) { - if (!filter) return null; - - const must = []; - - if (filter.q) { - must.push({ - function_score: { - boost_mode: 'multiply', - query: { - bool: { - should: [ - { - multi_match: { - query: filter.q, - fields: ['shortName^70', 'name^50', 'nameKZ^40', '_all'], - type: 'phrase', // or keyword - slop: 100, - boost: 10, - }, - }, - { - multi_match: { - query: filter.q, - fields: ['name^50', '_all'], - operator: 'and', - }, - }, - ], - }, - }, - }, - }); - } - - if (must.length > 0) { - return { bool: { must } }; - } - return null; -} - -GovStatBinEsTC.wrapResolver('searchConnection', resolver => { - resolver.addArgs({ - filter: 'JSON', - withAggs: 'Boolean', - }); - - return resolver.wrapResolve(next => rp => { - if (rp.args) { - if (rp.args.filter) { - rp.args.query = getQuery(rp.args.filter); - delete rp.args.filter; - } - } - return next(rp); - }); -}); diff --git a/__fixtures__/schemaElastic/elasticAwsConnection.js b/__fixtures__/schemaElastic/elasticAwsConnection.js deleted file mode 100644 index 5941fdb..0000000 --- a/__fixtures__/schemaElastic/elasticAwsConnection.js +++ /dev/null @@ -1,131 +0,0 @@ -/* @flow */ -/* eslint-disable no-restricted-syntax, guard-for-in, func-names */ - -/** - * A Connection handler for Amazon ES. - * - * Uses the aws-sdk to make signed requests to an Amazon ES endpoint. - * Define the Amazon ES config and the connection handler - * in the client configuration: - * - * var es = require('elasticsearch').Client({ - * hosts: 'https://amazon-es-host.us-east-1.es.amazonaws.com', - * connectionClass: require('http-aws-es'), - * amazonES: { - * region: 'us-east-1', - * accessKey: 'AKID', - * secretKey: 'secret', - * credentials: new AWS.EnvironmentCredentials('AWS') // Optional - * } - * }); - * - * @param client {Client} - The Client that this class belongs to - * @param config {Object} - Configuration options - * @param [config.protocol=http:] {String} - The HTTP protocol that this connection will use, can be set to https: - * @class HttpConnector - */ -import AWS from 'aws-sdk'; -import HttpConnector from 'elasticsearch/src/lib/connectors/http'; -import _ from 'elasticsearch/src/lib/utils'; -import zlib from 'zlib'; - -export default class HttpAmazonESConnector extends HttpConnector { - constructor(host: Object, config: Object) { - super(host, config); - this.endpoint = new AWS.Endpoint(host.host); - const c = config.amazonES; - if (c.credentials) { - this.creds = c.credentials; - } else { - this.creds = new AWS.Credentials(c.accessKey, c.secretKey); - } - this.amazonES = c; - } - - request(params: Object, cb: Function) { - let incoming; - let timeoutId; - let req; - let status = 0; - let headers = {}; - const log = this.log; - let response; - - const reqParams = this.makeReqParams(params); - // general clean-up procedure to run after the request - // completes, has an error, or is aborted. - const cleanUp = _.bind(err => { - clearTimeout(timeoutId); - - if (req) { - req.removeAllListeners(); - } - - if (incoming) { - incoming.removeAllListeners(); - } - - if (err instanceof Error === false) { - err = void 0; // eslint-disable-line - } - - log.trace(params.method, reqParams, params.body, response, status); - if (err) { - cb(err); - } else { - cb(err, response, status, headers); - } - }, this); - - const request = new AWS.HttpRequest(this.endpoint); - - // copy across params - for (const p in reqParams) { - request[p] = reqParams[p]; - } - request.region = this.amazonES.region; - if (params.body) request.body = params.body; - if (!request.headers) request.headers = {}; - request.headers['presigned-expires'] = false; - request.headers.Host = this.endpoint.host; - - // Sign the request (Sigv4) - const signer = new AWS.Signers.V4(request, 'es'); - signer.addAuthorization(this.creds, new Date()); - - const send = new AWS.NodeHttpClient(); - req = send.handleRequest( - request, - null, - _incoming => { - incoming = _incoming; - status = incoming.statusCode; - headers = incoming.headers; - response = ''; - - const encoding = (headers['content-encoding'] || '').toLowerCase(); - if (encoding === 'gzip' || encoding === 'deflate') { - incoming = incoming.pipe(zlib.createUnzip()); - } - - incoming.setEncoding('utf8'); - incoming.on('data', d => { - response += d; - }); - - incoming.on('error', cleanUp); - incoming.on('end', cleanUp); - }, - cleanUp - ); - - req.on('error', cleanUp); - - req.setNoDelay(true); - req.setSocketKeepAlive(true); - - return function() { - req.abort(); - }; - } -} diff --git a/__fixtures__/schemaElastic/elasticClient.js b/__fixtures__/schemaElastic/elasticClient.js deleted file mode 100644 index 7a378ff..0000000 --- a/__fixtures__/schemaElastic/elasticClient.js +++ /dev/null @@ -1,32 +0,0 @@ -// @flow - -import elasticsearch from 'elasticsearch'; -import awsElasticConnection from './elasticAwsConnection'; - -const ELASTIC_HOST = ``; -const elasticClient = new elasticsearch.Client({ - host: ELASTIC_HOST, - connectionClass: awsElasticConnection, - amazonES: { - region: /([^.]+).es.amazonaws.com/.exec(ELASTIC_HOST)[1], - accessKey: '', - secretKey: '', - }, - apiVersion: '5.0', - // log: 'trace', -}); - -// elasticClient.ping( -// { -// requestTimeout: 30000, -// }, -// error => { -// if (error) { -// console.error(' elasticsearch cluster is down'); // eslint-disable-line no-console -// } else { -// console.log(' connected to elasticsearch'); // eslint-disable-line no-console -// } -// } -// ); - -export default elasticClient; diff --git a/package.json b/package.json index 7770d55..2f51f92 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,6 @@ "docker:v2": "node ./scripts/docker/start 2 & wait", "docker:v5": "node ./scripts/docker/start 5 & wait", "link": "yarn build && yarn link graphql-compose && yarn link", - "unlink": "yarn unlink graphql-compose && yarn add graphql-compose", - "my-demo": "nodemon --exec babel-node ./__fixtures__/index.js" + "unlink": "yarn unlink graphql-compose && yarn add graphql-compose" } } diff --git a/src/composeWithElastic.js b/src/composeWithElastic.js index 251ec5e..4b0e9b0 100644 --- a/src/composeWithElastic.js +++ b/src/composeWithElastic.js @@ -5,6 +5,7 @@ import { convertToSourceTC, inputPropertiesToGraphQLTypes } from './mappingConve import createSearchResolver from './resolvers/search'; import createSearchConnectionResolver from './resolvers/searchConnection'; import createFindByIdResolver from './resolvers/findById'; +import createUpdateByIdResolver from './resolvers/updateById'; import type { ElasticMappingT } from './mappingConverter'; @@ -65,10 +66,12 @@ export function composeWithElastic(opts: composeWithElasticOptsT): TypeComposer const searchR = createSearchResolver(fieldMap, sourceTC, opts); const searchConnectionR = createSearchConnectionResolver(searchR, opts); const findByIdR = createFindByIdResolver(fieldMap, sourceTC, opts); + const updateByIdR = createUpdateByIdResolver(fieldMap, sourceTC, opts); sourceTC.addResolver(searchR); sourceTC.addResolver(searchConnectionR); sourceTC.addResolver(findByIdR); + sourceTC.addResolver(updateByIdR); return sourceTC; } diff --git a/src/resolvers/__tests__/findById-test.js b/src/resolvers/__tests__/findById-test.js index da7e29b..769a1a0 100644 --- a/src/resolvers/__tests__/findById-test.js +++ b/src/resolvers/__tests__/findById-test.js @@ -1,7 +1,7 @@ /* @flow */ import { Resolver } from 'graphql-compose'; -import createFindByIdResolver, * as FindById from '../findById'; +import createFindByIdResolver from '../findById'; import elasticClient from '../../__mocks__/elasticClient'; import { CvTC, CvFieldMap } from '../../__mocks__/cv'; @@ -25,13 +25,4 @@ describe('findById', () => { console.log(res); // eslint-disable-line }); }); - - it('toDottedList()', () => { - expect(FindById.toDottedList({ a: { b: true, c: { e: true } }, d: true })).toEqual([ - 'a.b', - 'a.c.e', - 'd', - ]); - expect(FindById.toDottedList({})).toEqual(true); - }); }); diff --git a/src/resolvers/findById.js b/src/resolvers/findById.js index da0b1f9..4ff6e9e 100644 --- a/src/resolvers/findById.js +++ b/src/resolvers/findById.js @@ -1,13 +1,12 @@ /* @flow */ -/* eslint-disable no-param-reassign */ -import { Resolver, TypeComposer, isObject } from 'graphql-compose'; -import type { ResolveParams, ProjectionType } from 'graphql-compose'; +import { Resolver, TypeComposer } from 'graphql-compose'; +import type { ResolveParams } from 'graphql-compose'; import type { FieldsMapByElasticType } from '../mappingConverter'; import ElasticApiParser from '../ElasticApiParser'; import { getFindByIdOutputTC } from '../types/FindByIdOutput'; -export type ElasticFindByIdResolverOpts = { +export type ElasticResolverOpts = { prefix?: ?string, elasticIndex: string, elasticType: string, @@ -17,7 +16,7 @@ export type ElasticFindByIdResolverOpts = { export default function createFindByIdResolver( fieldMap: FieldsMapByElasticType, sourceTC: TypeComposer, - opts: ElasticFindByIdResolverOpts + opts: ElasticResolverOpts ): Resolver { if (!fieldMap || !fieldMap._all) { throw new Error( @@ -36,10 +35,9 @@ export default function createFindByIdResolver( prefix, }); - const findByIdFC = parser.generateFieldConfig('getSource', { + const findByIdFC = parser.generateFieldConfig('get', { index: opts.elasticIndex, type: opts.elasticType, - _source: true, }); const argsConfigMap = Object.assign({}, findByIdFC.args); @@ -60,38 +58,22 @@ export default function createFindByIdResolver( kind: 'query', args: argsConfigMap, resolve: async (rp: ResolveParams<*, *>) => { - // const projection = rp.projection || {}; - const res = await findByIdFC.resolve(rp.source, rp.args, rp.context, rp.info); - console.log(res); + const { source, args, context, info } = rp; + + if (!args.id) { + throw new Error(`Missed 'id' argument!`); + } + + const res = await findByIdFC.resolve(source, args, context, info); + const { _index, _type, _id, _version, _source } = res || {}; return { - _index: opts.elasticIndex, - _type: opts.elasticType, - _id: rp.args.id, - _version: 1, - found: !!res, - _source: res, + _index, + _type, + _id, + _version, + ..._source, }; }, }); } - -export function toDottedList(projection: ProjectionType, prev?: string[]): string[] | boolean { - let result = []; - Object.keys(projection).forEach(k => { - if (isObject(projection[k])) { - const tmp = toDottedList(projection[k], prev ? [...prev, k] : [k]); - if (Array.isArray(tmp)) { - result = result.concat(tmp); - return; - } - } - - if (prev) { - result.push([...prev, k].join('.')); - } else { - result.push(k); - } - }); - return result.length > 0 ? result : true; -} diff --git a/src/resolvers/search.js b/src/resolvers/search.js index 75dcae6..2dc55d2 100644 --- a/src/resolvers/search.js +++ b/src/resolvers/search.js @@ -8,7 +8,7 @@ import ElasticApiParser from '../ElasticApiParser'; import { getSearchBodyITC, prepareBodyInResolve } from '../elasticDSL/SearchBody'; import { getSearchOutputTC } from '../types/SearchOutput'; -export type ElasticSearchResolverOpts = { +export type ElasticResolverOpts = { prefix?: ?string, elasticIndex: string, elasticType: string, @@ -18,7 +18,7 @@ export type ElasticSearchResolverOpts = { export default function createSearchResolver( fieldMap: FieldsMapByElasticType, sourceTC: TypeComposer, - opts: ElasticSearchResolverOpts + opts: ElasticResolverOpts ): Resolver { if (!fieldMap || !fieldMap._all) { throw new Error( @@ -180,8 +180,6 @@ export default function createSearchResolver( const res: any = await searchFC.resolve(rp.source, args, rp.context, rp.info); - console.log(res); - res.count = res.hits.total; res.max_score = res.hits.max_score; res.hits = res.hits.hits; diff --git a/src/resolvers/updateById.js b/src/resolvers/updateById.js new file mode 100644 index 0000000..1bbef34 --- /dev/null +++ b/src/resolvers/updateById.js @@ -0,0 +1,111 @@ +/* @flow */ + +import { Resolver, TypeComposer, InputTypeComposer } from 'graphql-compose'; +import type { ResolveParams } from 'graphql-compose'; +import type { FieldsMapByElasticType } from '../mappingConverter'; +import ElasticApiParser from '../ElasticApiParser'; +import { getUpdateByIdOutputTC } from '../types/UpdateByIdOutput'; +import { getTypeName, getOrSetType, desc } from '../utils'; + +export type ElasticResolverOpts = { + prefix?: ?string, + elasticIndex: string, + elasticType: string, + elasticClient: Object, +}; + +export default function createUpdateByIdResolver( + fieldMap: FieldsMapByElasticType, + sourceTC: TypeComposer, + opts: ElasticResolverOpts +): Resolver { + if (!fieldMap || !fieldMap._all) { + throw new Error( + 'First arg for Resolver updateById() should be fieldMap of FieldsMapByElasticType type.' + ); + } + + if (!sourceTC || sourceTC.constructor.name !== 'TypeComposer') { + throw new Error('Second arg for Resolver updateById() should be instance of TypeComposer.'); + } + + const prefix = opts.prefix || 'Es'; + + const parser = new ElasticApiParser({ + elasticClient: opts.elasticClient, + prefix, + }); + + const updateByIdFC = parser.generateFieldConfig('update', { + index: opts.elasticIndex, + type: opts.elasticType, + _source: true, + }); + + const argsConfigMap = Object.assign({}, updateByIdFC.args); + + argsConfigMap.record = { + type: getRecordITC(fieldMap), + }; + + const topLevelArgs = ['id', 'record']; + + Object.keys(argsConfigMap).forEach(argKey => { + if (topLevelArgs.indexOf(argKey) === -1) { + delete argsConfigMap[argKey]; + } + }); + + const type = getUpdateByIdOutputTC({ prefix, fieldMap, sourceTC }); + + return new Resolver({ + type, + name: 'updateById', + kind: 'mutation', + args: argsConfigMap, + resolve: async (rp: ResolveParams<*, *>) => { + const { source, args, context, info } = rp; + + if (!args.record) { + throw new Error(`Missed 'record' argument!`); + } + + if (!args.id) { + throw new Error(`Missed 'id' argument!`); + } + + args.body = { + doc: { + ...args.record, + }, + }; + delete args.record; + + const res = await updateByIdFC.resolve(source, args, context, info); + + const { _index, _type, _id, _version, result, get } = res || {}; + const { _source } = get || {}; + + return { + _id, + _index, + _type, + _version, + result, + ..._source, + }; + }, + }); +} + +export function getRecordITC(fieldMap: FieldsMapByElasticType): InputTypeComposer { + const name = getTypeName('Record', {}); + const description = desc(`The record from Elastic Search`); + return getOrSetType(name, () => + InputTypeComposer.create({ + name, + description, + fields: { ...fieldMap._all }, + }) + ); +} diff --git a/src/types/FindByIdOutput.js b/src/types/FindByIdOutput.js index c78b4ae..5ca88ba 100644 --- a/src/types/FindByIdOutput.js +++ b/src/types/FindByIdOutput.js @@ -4,26 +4,25 @@ import { TypeComposer } from 'graphql-compose'; import { getTypeName, getOrSetType } from '../utils'; import type { FieldsMapByElasticType } from '../mappingConverter'; -export type SearchOptsT = { +export type FindByIdOptsT = { prefix?: string, postfix?: string, fieldMap?: FieldsMapByElasticType, - sourceTC?: TypeComposer, + sourceTC: TypeComposer, }; -export function getFindByIdOutputTC(opts: SearchOptsT = {}): TypeComposer { +export function getFindByIdOutputTC(opts: FindByIdOptsT): TypeComposer { const name = getTypeName('FindByIdOutput', opts); - + const { sourceTC } = opts || {}; return getOrSetType(name, () => TypeComposer.create({ name, fields: { + _id: 'String', _index: 'String', _type: 'String', - _id: 'String', _version: 'Int', - _found: 'Boolean', - _source: opts.sourceTC || 'JSON', + ...sourceTC.getFields(), }, }) ); diff --git a/src/types/UpdateByIdOutput.js b/src/types/UpdateByIdOutput.js new file mode 100644 index 0000000..788b1dd --- /dev/null +++ b/src/types/UpdateByIdOutput.js @@ -0,0 +1,30 @@ +/* @flow */ + +import { TypeComposer } from 'graphql-compose'; +import { getTypeName, getOrSetType } from '../utils'; +import type { FieldsMapByElasticType } from '../mappingConverter'; + +export type UpdateByIdOptsT = { + prefix?: string, + postfix?: string, + fieldMap?: FieldsMapByElasticType, + sourceTC: TypeComposer, +}; + +export function getUpdateByIdOutputTC(opts: UpdateByIdOptsT): TypeComposer { + const name = getTypeName('UpdateByIdOutput', opts); + const { sourceTC } = opts || {}; + return getOrSetType(name, () => + TypeComposer.create({ + name, + fields: { + _id: 'String', + _index: 'String', + _type: 'String', + _version: 'Int', + result: 'String', + ...sourceTC.getFields(), + }, + }) + ); +} From 83f9f52c883c6c3d325f3eb0177e388d53c7e602 Mon Sep 17 00:00:00 2001 From: Borodayev Valeriy Date: Thu, 12 Apr 2018 16:22:26 +0600 Subject: [PATCH 7/7] refactor: refactor 'findById' & 'updateById' resolvers --- .gitignore | 1 + package.json | 3 ++- src/resolvers/findById.js | 12 +++--------- src/resolvers/updateById.js | 26 ++++---------------------- 4 files changed, 10 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 7eb80f0..c96954e 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ node_modules # Transpiled code /es /lib +/__fixtures__ coverage .nyc_output diff --git a/package.json b/package.json index 2f51f92..3a8dfc6 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "docker:v2": "node ./scripts/docker/start 2 & wait", "docker:v5": "node ./scripts/docker/start 5 & wait", "link": "yarn build && yarn link graphql-compose && yarn link", - "unlink": "yarn unlink graphql-compose && yarn add graphql-compose" + "unlink": "yarn unlink graphql-compose && yarn add graphql-compose", + "my-demo": "./node_modules/.bin/babel-node ./__fixtures__/index.js" } } diff --git a/src/resolvers/findById.js b/src/resolvers/findById.js index 4ff6e9e..58778d3 100644 --- a/src/resolvers/findById.js +++ b/src/resolvers/findById.js @@ -40,15 +40,9 @@ export default function createFindByIdResolver( type: opts.elasticType, }); - const argsConfigMap = Object.assign({}, findByIdFC.args); - - const topLevelArgs = ['id']; - - Object.keys(argsConfigMap).forEach(argKey => { - if (topLevelArgs.indexOf(argKey) === -1) { - delete argsConfigMap[argKey]; - } - }); + const argsConfigMap = { + id: 'String!', + }; const type = getFindByIdOutputTC({ prefix, fieldMap, sourceTC }); diff --git a/src/resolvers/updateById.js b/src/resolvers/updateById.js index 1bbef34..cf9947b 100644 --- a/src/resolvers/updateById.js +++ b/src/resolvers/updateById.js @@ -1,7 +1,6 @@ /* @flow */ -import { Resolver, TypeComposer, InputTypeComposer } from 'graphql-compose'; -import type { ResolveParams } from 'graphql-compose'; +import { Resolver, TypeComposer, InputTypeComposer, type ResolveParams } from 'graphql-compose'; import type { FieldsMapByElasticType } from '../mappingConverter'; import ElasticApiParser from '../ElasticApiParser'; import { getUpdateByIdOutputTC } from '../types/UpdateByIdOutput'; @@ -42,20 +41,11 @@ export default function createUpdateByIdResolver( _source: true, }); - const argsConfigMap = Object.assign({}, updateByIdFC.args); - - argsConfigMap.record = { - type: getRecordITC(fieldMap), + const argsConfigMap = { + id: 'String!', + record: getRecordITC(fieldMap).getTypeAsRequired(), }; - const topLevelArgs = ['id', 'record']; - - Object.keys(argsConfigMap).forEach(argKey => { - if (topLevelArgs.indexOf(argKey) === -1) { - delete argsConfigMap[argKey]; - } - }); - const type = getUpdateByIdOutputTC({ prefix, fieldMap, sourceTC }); return new Resolver({ @@ -66,14 +56,6 @@ export default function createUpdateByIdResolver( resolve: async (rp: ResolveParams<*, *>) => { const { source, args, context, info } = rp; - if (!args.record) { - throw new Error(`Missed 'record' argument!`); - } - - if (!args.id) { - throw new Error(`Missed 'id' argument!`); - } - args.body = { doc: { ...args.record,