Files
elasticsearch-js/scripts/utils/generate.js
Tomas Della Vedova 796644fc0c feat: add support for querystring in options object (#779)
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)
```
2019-03-19 10:34:12 +01:00

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