In very few cases, some API uses the same key for both url and query params, such as the bulk method. The client is not designed to handle such cases since accepts both url and query keys in the same object, and the url parameter will always take precedence. This pr fixes this edge case by adding a `querystring` key in the options object. Fixes: https://github.com/elastic/elasticsearch-js/pull/778 ```js client.bulk({ index: 'index', type: '_doc', body: [...] }, { querystring: { type: '_doc' } }, console.log) ```
551 lines
15 KiB
JavaScript
551 lines
15 KiB
JavaScript
/*
|
|
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
* license agreements. See the NOTICE file distributed with
|
|
* this work for additional information regarding copyright
|
|
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
* the Apache License, Version 2.0 (the "License"); you may
|
|
* not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
'use strict'
|
|
|
|
const dedent = require('dedent')
|
|
const allowedMethods = {
|
|
noBody: ['GET', 'HEAD', 'DELETE'],
|
|
body: ['POST', 'PUT', 'DELETE']
|
|
}
|
|
|
|
// list of apis that does not need any kind of validation
|
|
// because of how the url is built or the `type` handling in ES7
|
|
const noPathValidation = [
|
|
'create',
|
|
'exists',
|
|
'explain',
|
|
'get',
|
|
'get_source',
|
|
'indices.get_alias',
|
|
'indices.exists_alias',
|
|
'indices.get_field_mapping',
|
|
'indices.get_mapping',
|
|
'indices.get_settings',
|
|
'indices.put_mapping',
|
|
'indices.stats',
|
|
'nodes.info',
|
|
'nodes.stats',
|
|
'nodes.usage',
|
|
'tasks.cancel',
|
|
'termvectors',
|
|
'update'
|
|
]
|
|
|
|
// apis that uses bulkBody property
|
|
const ndjsonApi = [
|
|
'bulk',
|
|
'msearch',
|
|
'msearch_template',
|
|
'ml.find_file_structure',
|
|
'monitoring.bulk',
|
|
'xpack.ml.find_file_structure',
|
|
'xpack.monitoring.bulk'
|
|
]
|
|
|
|
function generate (spec, common) {
|
|
const api = Object.keys(spec)[0]
|
|
const name = api
|
|
.replace(/\.([a-z])/g, k => k[1].toUpperCase())
|
|
.replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
|
|
const methods = spec[api].methods
|
|
const { paths, parts, params } = spec[api].url
|
|
const acceptedQuerystring = []
|
|
const required = []
|
|
|
|
for (const key in parts) {
|
|
if (parts[key].required) {
|
|
required.push(key)
|
|
}
|
|
}
|
|
|
|
for (const key in params) {
|
|
if (params[key].required) {
|
|
required.push(key)
|
|
}
|
|
acceptedQuerystring.push(key)
|
|
}
|
|
|
|
for (const key in spec[api]) {
|
|
const k = spec[api][key]
|
|
if (k && k.required) {
|
|
required.push(key)
|
|
}
|
|
}
|
|
if (common && common.params) {
|
|
for (const key in common.params) {
|
|
acceptedQuerystring.push(key)
|
|
}
|
|
}
|
|
|
|
const code = `
|
|
function ${safeWords(name)} (params, options, callback) {
|
|
options = options || {}
|
|
if (typeof options === 'function') {
|
|
callback = options
|
|
options = {}
|
|
}
|
|
if (typeof params === 'function' || params == null) {
|
|
callback = params
|
|
params = {}
|
|
options = {}
|
|
}
|
|
|
|
// promises support
|
|
if (callback == null) {
|
|
return new Promise((resolve, reject) => {
|
|
${safeWords(name)}(params, options, (err, body) => {
|
|
err ? reject(err) : resolve(body)
|
|
})
|
|
})
|
|
}
|
|
|
|
${genRequiredChecks()}
|
|
|
|
${genUrlValidation(paths, api)}
|
|
|
|
// validate headers object
|
|
if (options.headers != null && typeof options.headers !== 'object') {
|
|
return callback(
|
|
new ConfigurationError(\`Headers should be an object, instead got: \${typeof options.headers}\`),
|
|
result
|
|
)
|
|
}
|
|
|
|
var warnings = null
|
|
var { ${genQueryBlacklist(false)} } = params
|
|
var querystring = semicopy(params, [${genQueryBlacklist()}])
|
|
|
|
if (method == null) {
|
|
${generatePickMethod(methods)}
|
|
}
|
|
|
|
var ignore = options.ignore || null
|
|
if (typeof ignore === 'number') {
|
|
ignore = [ignore]
|
|
}
|
|
|
|
|
|
var path = ''
|
|
${buildPath(api)}
|
|
|
|
// build request object
|
|
const request = {
|
|
method,
|
|
path,
|
|
${genBody(api, methods, spec[api].body)}
|
|
querystring
|
|
}
|
|
|
|
const requestOptions = {
|
|
ignore,
|
|
requestTimeout: options.requestTimeout || null,
|
|
maxRetries: options.maxRetries || null,
|
|
asStream: options.asStream || false,
|
|
headers: options.headers || null,
|
|
querystring: options.querystring || null,
|
|
compression: options.compression || false,
|
|
warnings
|
|
}
|
|
|
|
return makeRequest(request, requestOptions, callback)
|
|
|
|
function semicopy (obj, exclude) {
|
|
var target = {}
|
|
var keys = Object.keys(obj)
|
|
for (var i = 0, len = keys.length; i < len; i++) {
|
|
var key = keys[i]
|
|
if (exclude.indexOf(key) === -1) {
|
|
target[snakeCase[key] || key] = obj[key]
|
|
if (acceptedQuerystring.indexOf(snakeCase[key] || key) === -1) {
|
|
warnings = warnings || []
|
|
warnings.push('Client - Unknown parameter: "' + key + '", sending it as query parameter')
|
|
}
|
|
}
|
|
}
|
|
return target
|
|
}
|
|
}
|
|
`.trim() // always call trim to avoid newlines
|
|
|
|
const fn = dedent`
|
|
/*
|
|
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
* license agreements. See the NOTICE file distributed with
|
|
* this work for additional information regarding copyright
|
|
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
* the Apache License, Version 2.0 (the "License"); you may
|
|
* not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
'use strict'
|
|
|
|
/* eslint camelcase: 0 */
|
|
/* eslint no-unused-vars: 0 */
|
|
|
|
function build${name[0].toUpperCase() + name.slice(1)} (opts) {
|
|
// eslint-disable-next-line no-unused-vars
|
|
const { makeRequest, ConfigurationError, result } = opts
|
|
${generateDocumentation(spec[api], api)}
|
|
|
|
const acceptedQuerystring = [
|
|
${acceptedQuerystring.map(q => `'${q}'`).join(',\n')}
|
|
]
|
|
|
|
const snakeCase = {
|
|
${genSnakeCaseMap()}
|
|
}
|
|
|
|
return ${code}
|
|
}
|
|
|
|
module.exports = build${name[0].toUpperCase() + name.slice(1)}
|
|
`
|
|
|
|
// new line at the end of file
|
|
return fn + '\n'
|
|
|
|
function genRequiredChecks (param) {
|
|
const code = required
|
|
.map(_genRequiredCheck)
|
|
.concat(_noBody())
|
|
.filter(Boolean)
|
|
|
|
if (code.length) {
|
|
code.unshift('// check required parameters')
|
|
}
|
|
|
|
return code.join('\n ')
|
|
|
|
function _genRequiredCheck (param) {
|
|
var camelCased = param[0] === '_'
|
|
? '_' + param.slice(1).replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
: param.replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
|
|
if (param === camelCased) {
|
|
const check = `
|
|
if (params['${param}'] == null) {
|
|
return callback(
|
|
new ConfigurationError('Missing required parameter: ${param}'),
|
|
result
|
|
)
|
|
}
|
|
`
|
|
return check.trim()
|
|
} else {
|
|
const check = `
|
|
if (params['${param}'] == null && params['${camelCased}'] == null) {
|
|
return callback(
|
|
new ConfigurationError('Missing required parameter: ${param} or ${camelCased}'),
|
|
result
|
|
)
|
|
}
|
|
`
|
|
return check.trim()
|
|
}
|
|
}
|
|
|
|
function _noBody () {
|
|
const check = `
|
|
if (params.body != null) {
|
|
return callback(
|
|
new ConfigurationError('This API does not require a body'),
|
|
result
|
|
)
|
|
}
|
|
`
|
|
return spec[api].body === null ? check.trim() : ''
|
|
}
|
|
}
|
|
|
|
function genSnakeCaseMap () {
|
|
const toCamelCase = str => {
|
|
return str[0] === '_'
|
|
? '_' + str.slice(1).replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
: str.replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
}
|
|
|
|
return acceptedQuerystring.reduce((acc, val, index) => {
|
|
if (toCamelCase(val) !== val) {
|
|
acc += `${toCamelCase(val)}: '${val}'`
|
|
if (index !== acceptedQuerystring.length - 1) {
|
|
acc += ',\n'
|
|
}
|
|
}
|
|
return acc
|
|
}, '')
|
|
}
|
|
|
|
function genQueryBlacklist (addQuotes = true) {
|
|
const toCamelCase = str => {
|
|
return str[0] === '_'
|
|
? '_' + str.slice(1).replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
: str.replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
}
|
|
|
|
const blacklist = ['method', 'body']
|
|
if (typeof parts === 'object' && parts !== null) {
|
|
Object.keys(parts).forEach(p => {
|
|
const camelStr = toCamelCase(p)
|
|
if (camelStr !== p) blacklist.push(`${camelStr}`)
|
|
blacklist.push(`${p}`)
|
|
})
|
|
}
|
|
return addQuotes ? blacklist.map(q => `'${q}'`) : blacklist
|
|
}
|
|
|
|
function buildPath () {
|
|
const toCamelCase = str => {
|
|
return str[0] === '_'
|
|
? '_' + str.slice(1).replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
: str.replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
}
|
|
|
|
const genAccessKey = str => {
|
|
const camelStr = toCamelCase(str)
|
|
return camelStr === str
|
|
? str
|
|
: `${str} || ${camelStr}`
|
|
}
|
|
|
|
const genCheck = path => {
|
|
return path
|
|
.split('/')
|
|
.filter(Boolean)
|
|
.map(p => p.startsWith('{') ? `(${genAccessKey(p.slice(1, -1))}) != null` : false)
|
|
.filter(Boolean)
|
|
.join(' && ')
|
|
}
|
|
|
|
const genPath = path => {
|
|
path = path
|
|
.split('/')
|
|
.filter(Boolean)
|
|
.map(p => p.startsWith('{') ? `encodeURIComponent(${genAccessKey(p.slice(1, -1))})` : `'${p}'`)
|
|
.join(' + \'/\' + ')
|
|
return path.length > 0 ? ('\'/\' + ' + path) : '\'/\''
|
|
}
|
|
|
|
var code = ''
|
|
var hasStaticPath = false
|
|
var singlePathComponent = false
|
|
paths
|
|
.filter(path => {
|
|
if (path.indexOf('{') > -1) return true
|
|
if (hasStaticPath === false) {
|
|
hasStaticPath = true
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
.sort((a, b) => (b.split('{').length + b.split('/').length) - (a.split('{').length + a.split('/').length))
|
|
.forEach((path, index, arr) => {
|
|
if (arr.length === 1) {
|
|
singlePathComponent = true
|
|
code += `
|
|
path = ${genPath(path)}
|
|
`
|
|
} else if (index === 0) {
|
|
code += `
|
|
if (${genCheck(path)}) {
|
|
path = ${genPath(path)}
|
|
`
|
|
} else if (index === arr.length - 1) {
|
|
code += `
|
|
} else {
|
|
path = ${genPath(path)}
|
|
`
|
|
} else {
|
|
code += `
|
|
} else if (${genCheck(path)}) {
|
|
path = ${genPath(path)}
|
|
`
|
|
}
|
|
})
|
|
|
|
code += singlePathComponent ? '' : '}'
|
|
return code
|
|
}
|
|
}
|
|
|
|
function safeWords (str) {
|
|
switch (str) {
|
|
// delete is a reserved word
|
|
case 'delete':
|
|
return '_delete'
|
|
// index is also a parameter
|
|
case 'index':
|
|
return '_index'
|
|
default:
|
|
return str
|
|
}
|
|
}
|
|
|
|
function generatePickMethod (methods) {
|
|
if (methods.length === 1) {
|
|
return `method = '${methods[0]}'`
|
|
}
|
|
const bodyMethod = getBodyMethod(methods)
|
|
const noBodyMethod = getNoBodyMethod(methods)
|
|
if (bodyMethod && noBodyMethod) {
|
|
return `method = body == null ? '${noBodyMethod}' : '${bodyMethod}'`
|
|
} else if (bodyMethod) {
|
|
return `
|
|
method = '${bodyMethod}'
|
|
`.trim()
|
|
} else {
|
|
return `
|
|
method = '${noBodyMethod}'
|
|
`.trim()
|
|
}
|
|
}
|
|
|
|
function genBody (api, methods, body) {
|
|
const bodyMethod = getBodyMethod(methods)
|
|
if (ndjsonApi.indexOf(api) > -1) {
|
|
return 'bulkBody: body,'
|
|
}
|
|
if (body === null && bodyMethod) {
|
|
return `body: '',`
|
|
} else if (bodyMethod) {
|
|
return `body: body || '',`
|
|
} else {
|
|
return 'body: null,'
|
|
}
|
|
}
|
|
|
|
function getBodyMethod (methods) {
|
|
const m = methods.filter(m => ~allowedMethods.body.indexOf(m))
|
|
if (m.length) return m[0]
|
|
return null
|
|
}
|
|
|
|
function getNoBodyMethod (methods) {
|
|
const m = methods.filter(m => ~allowedMethods.noBody.indexOf(m))
|
|
if (m.length) return m[0]
|
|
return null
|
|
}
|
|
|
|
function genUrlValidation (paths, api) {
|
|
// this api does not need url validation
|
|
if (!needsPathValidation(api)) return ''
|
|
// gets only the dynamic components of the url in an array
|
|
// then we reverse it. A parameters always require what is
|
|
// at its right in the array.
|
|
const chunks = paths
|
|
.reduce((a, b) => a.split('/').length > b.split('/').length ? a : b)
|
|
.split('/')
|
|
.filter(s => s.startsWith('{'))
|
|
.map(s => s.slice(1, -1))
|
|
.reverse()
|
|
|
|
var code = ''
|
|
|
|
const len = chunks.length
|
|
chunks.forEach((chunk, index) => {
|
|
if (index === len - 1) return
|
|
var params = []
|
|
var camelCased = chunk[0] === '_'
|
|
? '_' + chunk.slice(1).replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
: chunk.replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
|
|
if (chunk === camelCased) {
|
|
code += `${index ? '} else ' : ''}if (params['${chunk}'] != null && (`
|
|
} else {
|
|
code += `${index ? '} else ' : ''}if ((params['${chunk}'] != null || params['${camelCased}'] != null) && (`
|
|
}
|
|
for (var i = index + 1; i < len; i++) {
|
|
params.push(chunks[i])
|
|
// url parts can be declared in camelCase fashion
|
|
camelCased = chunks[i][0] === '_'
|
|
? '_' + chunks[i].slice(1).replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
: chunks[i].replace(/_([a-z])/g, k => k[1].toUpperCase())
|
|
|
|
if (chunks[i] === camelCased) {
|
|
code += `params['${chunks[i]}'] == null${i === len - 1 ? '' : ' || '}`
|
|
} else {
|
|
code += `(params['${chunks[i]}'] == null && params['${camelCased}'] == null)${i === len - 1 ? '' : ' || '}`
|
|
}
|
|
}
|
|
code += `)) {
|
|
return callback(
|
|
new ConfigurationError('Missing required parameter of the url: ${params.join(', ')}'),
|
|
result
|
|
)`
|
|
})
|
|
|
|
if (chunks.length > 1) {
|
|
code += '\n}'
|
|
}
|
|
|
|
if (code.length) {
|
|
code = '// check required url components\n' + code
|
|
}
|
|
|
|
return code.trim()
|
|
}
|
|
|
|
function generateDocumentation (api, op) {
|
|
const { parts = {}, params = {} } = api.url
|
|
const { body } = api
|
|
|
|
// we use `replace(/\u00A0/g, ' ')` to remove no breaking spaces
|
|
// because some parts of the description fields are using it
|
|
|
|
var doc = '/**\n'
|
|
doc += ` * Perform a [${op}](${api.documentation}) request\n *\n`
|
|
Object.keys(parts).forEach(part => {
|
|
const obj = parts[part]
|
|
const description = obj.description || ''
|
|
doc += ` * @param {${obj.type}} ${part} - ${description.replace(/\u00A0/g, ' ')}\n`
|
|
})
|
|
|
|
Object.keys(params).forEach(param => {
|
|
const obj = params[param]
|
|
const description = obj.description || ''
|
|
doc += ` * @param {${obj.type}} ${param} - ${description.replace(/\u00A0/g, ' ')}\n`
|
|
})
|
|
|
|
if (body) {
|
|
const description = body.description || ''
|
|
doc += ` * @param {${body.type || 'object'}} body - ${description.replace(/\u00A0/g, ' ')}\n`
|
|
}
|
|
|
|
doc += ' */'
|
|
|
|
return doc
|
|
}
|
|
|
|
function needsPathValidation (api) {
|
|
return noPathValidation.indexOf(api) === -1
|
|
}
|
|
|
|
module.exports = generate
|