graphql-compose-elasticsearch
Version:
Elastic search via GraphQL
354 lines • 13.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const dox_1 = __importDefault(require("dox"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const graphql_compose_1 = require("graphql-compose");
const utils_1 = require("./utils");
class ElasticApiParser {
constructor(opts = {}) {
this.apiVersion =
opts.apiVersion ||
(opts.elasticClient &&
opts.elasticClient.transport &&
opts.elasticClient.transport._config &&
opts.elasticClient.transport._config.apiVersion) ||
'_default';
const apiFilePath = path_1.default.resolve(opts.elasticApiFilePath || ElasticApiParser.findApiVersionFile(this.apiVersion));
const source = ElasticApiParser.loadApiFile(apiFilePath);
this.parsedSource = ElasticApiParser.parseSource(source);
this.elasticClient = opts.elasticClient;
this.prefix = opts.prefix || 'Elastic';
this.cachedEnums = {};
}
static loadFile(absolutePath) {
return fs_1.default.readFileSync(absolutePath, 'utf8');
}
static loadApiFile(absolutePath) {
let code;
try {
code = ElasticApiParser.loadFile(absolutePath);
}
catch (e) {
throw new Error(`Cannot load Elastic API source file from ${absolutePath}`);
}
return ElasticApiParser.cleanUpSource(code);
}
static loadApiListFile(absolutePath) {
let code;
try {
code = ElasticApiParser.loadFile(absolutePath);
}
catch (e) {
throw new Error(`Cannot load Elastic API file with available versions from ${absolutePath}`);
}
return code;
}
static findApiVersionFile(version) {
const esModulePath = path_1.default.dirname(require.resolve('elasticsearch'));
const apiFolder = `${esModulePath}/lib/apis/`;
const apiListFile = path_1.default.resolve(apiFolder, 'index.js');
const apiListCode = ElasticApiParser.loadApiListFile(apiListFile);
const re = new RegExp(`\\'${version}\\'\\(\\).*require\\(\\'(.+)\\'\\)`, 'gi');
const match = re.exec(apiListCode);
if (match && match[1]) {
return path_1.default.resolve(apiFolder, `${match[1]}.js`);
}
const re12 = new RegExp(`\\'${version}\\':\\srequire\\(\\'(.+)\\'\\)`, 'gi');
const match12 = re12.exec(apiListCode);
if (match12 && match12[1]) {
return path_1.default.resolve(apiFolder, `${match12[1]}.js`);
}
throw new Error(`Can not found Elastic version '${version}' in ${apiListFile}`);
}
static cleanUpSource(code) {
let codeCleaned = code.replace(/{<<.+`(.*)`.+}/gi, '{$1}');
codeCleaned = codeCleaned.replace(/(api.*)\['(.+)'\](.*ca)/gi, '$1.$2$3');
return codeCleaned;
}
static parseParamsDescription(doxItemAST) {
const descriptions = {};
if (Array.isArray(doxItemAST.tags)) {
doxItemAST.tags.forEach((tag) => {
if (!tag || tag.type !== 'param')
return;
if (tag.name === 'params')
return;
const name = ElasticApiParser.cleanupParamName(tag.name);
if (!name)
return;
descriptions[name] = ElasticApiParser.cleanupDescription(tag.description);
});
}
return descriptions;
}
static cleanupDescription(str) {
if (typeof str === 'string') {
if (str.startsWith('- ')) {
str = str.substr(2);
}
str = str.trim();
return str;
}
return undefined;
}
static cleanupParamName(str) {
if (typeof str === 'string') {
if (str.startsWith('params.')) {
str = str.substr(7);
}
str = str.trim();
return str;
}
return undefined;
}
static codeToSettings(code) {
const reg = /ca\((\{(.|\n)+\})\);/g;
const matches = reg.exec(code);
if (matches && matches[1]) {
return eval('(' + matches[1] + ')');
}
return undefined;
}
static getMethodName(str) {
const parts = str.split('.');
if (parts[0] === 'api') {
parts.shift();
}
if (parts.length === 1) {
return parts[0];
}
else {
return parts.filter((o) => o !== 'prototype');
}
}
static parseSource(source) {
const result = {};
if (!source || typeof source !== 'string') {
throw Error('Empty source. It should be non-empty string.');
}
const doxAST = dox_1.default.parseComments(source, { raw: true });
if (!doxAST || !Array.isArray(doxAST)) {
throw Error('Incorrect response from dox.parseComments');
}
doxAST.forEach((item) => {
if (!item.ctx || !item.ctx.string) {
return;
}
let description;
if (item.description && item.description.full) {
description = ElasticApiParser.cleanupDescription(item.description.full);
}
const elasticMethod = ElasticApiParser.getMethodName(item.ctx.string);
const dottedMethodName = Array.isArray(elasticMethod)
? elasticMethod.join('.')
: elasticMethod;
result[dottedMethodName] = {
elasticMethod,
description,
argsSettings: ElasticApiParser.codeToSettings(item.code),
argsDescriptions: ElasticApiParser.parseParamsDescription(item),
};
});
return result;
}
generateFieldMap() {
const result = {};
Object.keys(this.parsedSource).forEach((methodName) => {
result[methodName] = this.generateFieldConfig(methodName);
});
const fieldMap = this.reassembleNestedFields(result);
return (0, utils_1.reorderKeys)(fieldMap, [
'cat',
'cluster',
'indices',
'ingest',
'nodes',
'snapshot',
'tasks',
'search',
]);
}
generateFieldConfig(methodName, methodArgs) {
if (!methodName) {
throw new Error(`You should provide Elastic search method.`);
}
if (!this.parsedSource[methodName]) {
throw new Error(`Elastic search method '${methodName}' does not exists.`);
}
const { description, argsSettings, argsDescriptions, elasticMethod } = this.parsedSource[methodName];
const argMap = this.settingsToArgMap(argsSettings, argsDescriptions);
return {
type: 'JSON',
description,
args: argMap,
resolve: (_source, args, context, _info) => {
const client = (context && context.elasticClient) || this.elasticClient;
if (!client) {
throw new Error('You should provide `elasticClient` when created types via ' +
'`opts.elasticClient` or in runtime via GraphQL context');
}
if (Array.isArray(elasticMethod)) {
return client[elasticMethod[0]][elasticMethod[1]](Object.assign(Object.assign({}, methodArgs), args));
}
return client[elasticMethod](Object.assign(Object.assign({}, methodArgs), args));
},
};
}
paramToGraphQLArgConfig(paramCfg, fieldName, description) {
const result = {
type: this.paramTypeToGraphQL(paramCfg, fieldName),
};
if (paramCfg.default) {
result.defaultValue = paramCfg.default;
if (result.type === 'Float' || result.type === 'Int') {
const defaultNumber = Number(result.defaultValue);
if (Number.isNaN(defaultNumber)) {
delete result.defaultValue;
}
else {
result.defaultValue = defaultNumber;
}
}
else if (result.type === 'Boolean') {
const t = result.defaultValue;
result.defaultValue = t === 'true' || t === '1' || t === true;
}
}
else if (fieldName === 'format') {
result.defaultValue = 'json';
}
if (description) {
result.description = description;
}
if (Array.isArray(result.defaultValue)) {
result.type = [result.type];
}
return result;
}
paramTypeToGraphQL(paramCfg, fieldName) {
switch (paramCfg.type) {
case 'string':
return 'String';
case 'boolean':
return 'Boolean';
case 'number':
return 'Float';
case 'time':
return 'String';
case 'list':
return 'JSON';
case 'enum':
if (Array.isArray(paramCfg.options)) {
return this.getEnumType(fieldName, paramCfg.options);
}
return 'String';
case undefined:
return 'JSON';
default:
return 'JSON';
}
}
getEnumType(fieldName, values) {
const key = fieldName;
const subKey = JSON.stringify(values);
if (!this.cachedEnums[key]) {
this.cachedEnums[key] = {};
}
if (!this.cachedEnums[key][subKey]) {
const enumValues = values.reduce((result, val) => {
if (val === '') {
result.empty_string = { value: '' };
}
else if (val === 'true') {
result.true_string = { value: 'true' };
}
else if (val === true) {
result.true_boolean = { value: true };
}
else if (val === 'false') {
result.false_string = { value: 'false' };
}
else if (val === false) {
result.false_boolean = { value: false };
}
else if (val === 'null') {
result.null_string = { value: 'null' };
}
else if (Number.isFinite(val)) {
result[`number_${val}`] = { value: val };
}
else if (typeof val === 'string') {
result[val] = { value: val };
}
return result;
}, {});
const n = Object.keys(this.cachedEnums[key]).length;
const postfix = n === 0 ? '' : `_${n}`;
this.cachedEnums[key][subKey] = graphql_compose_1.EnumTypeComposer.createTemp({
name: `${this.prefix}Enum_${(0, graphql_compose_1.upperFirst)(fieldName)}${postfix}`,
values: enumValues,
});
}
return this.cachedEnums[key][subKey];
}
settingsToArgMap(settings, descriptions = {}) {
const result = {};
const { params, urls, url, method, needBody } = settings || {};
if (method === 'POST' || method === 'PUT') {
result.body = {
type: needBody ? 'JSON!' : 'JSON',
};
}
if (params) {
Object.keys(params).forEach((k) => {
const fieldConfig = this.paramToGraphQLArgConfig(params[k], k, descriptions[k]);
if (fieldConfig) {
result[k] = fieldConfig;
}
});
}
const urlList = urls || (url ? [url] : null);
if (Array.isArray(urlList)) {
urlList.forEach((item) => {
if (item.req) {
Object.keys(item.req).forEach((k) => {
const fieldConfig = this.paramToGraphQLArgConfig(item.req[k], k, descriptions[k]);
if (fieldConfig) {
result[k] = fieldConfig;
}
});
}
});
}
return result;
}
reassembleNestedFields(fields) {
const result = {};
Object.keys(fields).forEach((k) => {
const names = k.split('.');
if (names.length === 1) {
result[names[0]] = fields[k];
}
else {
if (!result[names[0]]) {
result[names[0]] = {
type: graphql_compose_1.ObjectTypeComposer.createTemp({
name: `${this.prefix}_${(0, graphql_compose_1.upperFirst)(names[0])}`,
}),
resolve: () => {
return {};
},
};
}
result[names[0]].type.setField(names[1], fields[k]);
}
});
return result;
}
}
exports.default = ElasticApiParser;
//# sourceMappingURL=ElasticApiParser.js.map