Skip to content

Added Request/Response types definitions #970

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
783 changes: 436 additions & 347 deletions api/requestParams.d.ts → api/RequestTypes.d.ts

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions api/ResponseTypes.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

interface SearchResponse<TSource = unknown, TAggregations = unknown> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not meant to be used in api/kibana.d.ts?

took: number
timed_out: boolean
_scroll_id?: string
_shards: Shards
hits: {
total: {
value: number
relation: string
Copy link

@Veetaha Veetaha Apr 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
relation: string
relation: "eq" | "gte"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised that we are able to generate request types from the API spec, but no responses. https://github.com/elastic/elasticsearch/blob/d7bbc9df1d3edf47f8d1c2f0983c88c64d78a4b4/rest-api-spec/src/main/resources/rest-api-spec/api/search.json#L231
@delvedor Do you know if the elasticesearch team is going to introduce a spec for API responses as well?

}
max_score: number
hits: Array<{
Copy link

@Veetaha Veetaha Apr 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@delvedor I'd suggest extracting this inline object literal type into an interface. I in my code usually use not only the Source type, but the whole hit type because there is an id in it I'd like to use:

interface Doc<TSource> {
    _id: string;
    _source: TSource;
}

If we don't extract this into an interface we won't be able to write the following code:

interface MyDocSource { foo: number }
// The type of hit should be Doc<MyDocSource> but we don't have the Doc type as per this PR right now
function processHit(hit: ??) {
    console.log("doc with id", hit._id, "has foo:", hit._source.foo);
}

const res = await client.search<MyDocSource>({ ... });

// notice that we cannot annotate the docHit param type with an interface name here too
res.body.hits.hits.forEach(docHit => processHit(docHit))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 here. I hit this problem a couple of days ago. An example from Kibana codebase:
https://github.com/elastic/kibana/blob/master/x-pack/plugins/logstash/server/types.ts#L11-L12

_index: string
_type: string
_id: string
_score: number
_source: TSource
_version?: number
_explanation?: Explanation
fields?: Record<string, unknown>
highlight?: Record<string, string[]>
inner_hits?: Record<string, unknown>
matched_queries?: string[]
sort?: any[]
}>
}
aggregations?: TAggregations
}

interface Shards {
total: number
successful: number
failed: number
skipped: number
}

interface Explanation {
value: number
description: string
details: Explanation[]
}

interface MSearchResponse<TSource = unknown, TAggregations = unknown> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't see it's used in index.d.ts file

responses: Array<SearchResponse<TSource, TAggregations>>
}

interface CreateResponse {
_shards: Shards
_index: string
_type: string
_id: string
_version: number
_seq_no: number
_primary_term: number
result: string
}

interface IndexResponse extends CreateResponse {}

interface DeleteResponse {
_shards: Shards
_index: string
_type: string
_id: string
_version: number
_seq_no: number
_primary_term: number
result: string
}

interface UpdateResponse {
_shards: Shards
_index: string
_type: string
_id: string
_version: number
result: string
}
Comment on lines +75 to +82
Copy link

@Veetaha Veetaha Apr 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Elasticsearch 7 supports returning the document source in update requests, e.g. the following request

POST /<index>/_update/<id>
{
  "_source": "*",
  "doc": {
    "field": "val"
  }
}

gives the following response

{
  "_index" : "<index>",
  "_type" : "_doc",
  "_id" : "<id>",
  "_version" : 5,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 53,
  "_primary_term" : 1,
  "get" : {
    "_seq_no" : 53,
    "_primary_term" : 1,
    "found" : true,
    "_source" : {
      "field": "val"
    }
  }
}

So the user should be able to specify the type of that _source (furthermore this get object should be an optional property in UpdateResponse<T>.

@delvedor also note that the shards object I got from Elasticsearch doesn't contain skipped field, this is either a bug or an intended behavior, so if the latter we should mark it as optional: interface Shards { skipped?: number }

Also note that there are two more fields:

{
  "_seq_no" : 53,
  "_primary_term" : 1
}

I think they also should be included in interface UpdateResponse


interface GetResponse<TSource = unknown> {
_index: string
_type: string
_id: string
_version: number
_seq_no: number
_primary_term: number
found: boolean
_source: TSource
}

interface BulkResponse {
took: number
errors: boolean
items: BulkItem[]
}

type BulkItem =
| { index: BulkIndex }
| { create: BulkCreate }
| { update: BulkUpdate }
| { delete: BulkDelete }

interface BulkIndex extends IndexResponse {
status: number
}

interface BulkCreate extends CreateResponse {
status: number
}

interface BulkUpdate extends UpdateResponse {
status: number
}

interface BulkDelete extends DeleteResponse {
status: number
}

export {
SearchResponse,
MSearchResponse,
CreateResponse,
IndexResponse,
DeleteResponse,
UpdateResponse,
GetResponse,
BulkResponse
}
691 changes: 346 additions & 345 deletions api/kibana.d.ts

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions docs/typescript.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,63 @@ async function run () {

run().catch(console.log)
----

=== Response body definitions

Currently, there is no support for all the response definitions, the client offers only a small subset to help the users with the most commonly used APIs.

* `Index`
* `Create`
* `Update`
* `Delete`
* `Search`
* `MSearch`
* `Bulk`

_At the moment, we are not planning on expanding more the types offered out of the box since we are studying a more structured approach that will allow us to generate all the response type definitions automatically._

You can access the response type definitions via the `ResponseParams`.
Every API that contains a `_source` object accepts a https://www.typescriptlang.org/docs/handbook/generics.html[generics] which represents the type of the `_source` object, if you don't configure anything, it will default to `any`.

The example you saw above can now be rewritten as follows:
[source,ts]
----
import {
Client,
RequestParams,
ResponseParams,
ApiResponse,
} from '@elastic/elasticsearch'

const client = new Client({ node: 'http://localhost:9200' })

// Define the type of the body for the Search request
interface SearchBody {
query: {
match: { foo: string }
}
}

// Define the interface of the source object
interface Source {
foo: string
}

async function run (): Promise<void> {
// Define the search parameters
const searchParams: RequestParams.Search<SearchBody> = {
index: 'test',
body: {
query: {
match: { foo: 'bar' }
}
}
}

// Craft the final type definition
const response: ApiResponse<ResponseParams.Search<Source>> = await client.search(searchParams)
console.log(response.body)
}

run().catch(console.log)
----
3,686 changes: 1,844 additions & 1,842 deletions index.d.ts

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions lib/Helpers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

import { Readable as ReadableStream } from 'stream'
import { TransportRequestOptions, ApiError, ApiResponse, RequestBody, Context } from './Transport'
import { Search, Msearch, Bulk } from '../api/requestParams'
import { SearchRequest, BulkRequest, MsearchRequest } from '../api/RequestTypes'

export default class Helpers {
search<TDocument = unknown, TRequestBody extends RequestBody = Record<string, any>>(params: Search<TRequestBody>, options?: TransportRequestOptions): Promise<TDocument[]>
scrollSearch<TDocument = unknown, TResponse = Record<string, any>, TRequestBody extends RequestBody = Record<string, any>, TContext = Context>(params: Search<TRequestBody>, options?: TransportRequestOptions): AsyncIterable<ScrollSearchResponse<TDocument, TResponse, TContext>>
scrollDocuments<TDocument = unknown, TRequestBody extends RequestBody = Record<string, any>>(params: Search<TRequestBody>, options?: TransportRequestOptions): AsyncIterable<TDocument>
search<TDocument = unknown, TRequestBody extends RequestBody = Record<string, any>>(params: SearchRequest<TRequestBody>, options?: TransportRequestOptions): Promise<TDocument[]>
scrollSearch<TDocument = unknown, TResponse = Record<string, any>, TRequestBody extends RequestBody = Record<string, any>, TContext = Context>(params: SearchRequest<TRequestBody>, options?: TransportRequestOptions): AsyncIterable<ScrollSearchResponse<TDocument, TResponse, TContext>>
scrollDocuments<TDocument = unknown, TRequestBody extends RequestBody = Record<string, any>>(params: SearchRequest<TRequestBody>, options?: TransportRequestOptions): AsyncIterable<TDocument>
msearch(options?: MsearchHelperOptions): MsearchHelper
bulk<TDocument = unknown>(options: BulkHelperOptions<TDocument>): BulkHelper<BulkStats>
}
Expand Down Expand Up @@ -65,7 +65,7 @@ type UpdateAction = [UpdateActionOperation, Record<string, any>]
type Action = IndexAction | CreateAction | UpdateAction | DeleteAction
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

export interface BulkHelperOptions<TDocument = unknown> extends Omit<Bulk, 'body'> {
export interface BulkHelperOptions<TDocument = unknown> extends Omit<BulkRequest, 'body'> {
datasource: TDocument[] | Buffer | ReadableStream | AsyncIterator<TDocument>
onDocument: (doc: TDocument) => Action
flushBytes?: number
Expand All @@ -91,7 +91,7 @@ export interface OnDropDocument<TDocument = unknown> {
retried: boolean
}

export interface MsearchHelperOptions extends Omit<Msearch, 'body'> {
export interface MsearchHelperOptions extends Omit<MsearchRequest, 'body'> {
operations?: number
flushInterval?: number
concurrency?: number
Expand All @@ -102,6 +102,6 @@ export interface MsearchHelperOptions extends Omit<Msearch, 'body'> {
declare type callbackFn<Response, Context> = (err: ApiError, result: ApiResponse<Response, Context>) => void;
export interface MsearchHelper extends Promise<void> {
stop(error?: Error): void
search<TResponse = Record<string, any>, TRequestBody extends RequestBody = Record<string, any>, TContext = Context>(header: Omit<Search, 'body'>, body: TRequestBody): Promise<ApiResponse<TResponse, TContext>>
search<TResponse = Record<string, any>, TRequestBody extends RequestBody = Record<string, any>, TContext = Context>(header: Omit<Search, 'body'>, body: TRequestBody, callback: callbackFn<TResponse, TContext>): void
search<TResponse = Record<string, any>, TRequestBody extends RequestBody = Record<string, any>, TContext = Context>(header: Omit<SearchRequest, 'body'>, body: TRequestBody): Promise<ApiResponse<TResponse, TContext>>
search<TResponse = Record<string, any>, TRequestBody extends RequestBody = Record<string, any>, TContext = Context>(header: Omit<SearchRequest, 'body'>, body: TRequestBody, callback: callbackFn<TResponse, TContext>): void
}
2 changes: 1 addition & 1 deletion scripts/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function start (opts) {
const typeDefFile = join(__dirname, '..', 'index.d.ts')
const kibanaTypeDefFile = join(packageFolder, 'kibana.d.ts')
const docOutputFile = join(__dirname, '..', 'docs', 'reference.asciidoc')
const requestParamsOutputFile = join(packageFolder, 'requestParams.d.ts')
const requestParamsOutputFile = join(packageFolder, 'RequestTypes.d.ts')
const allSpec = []

log.text = 'Cleaning API folder...'
Expand Down
48 changes: 30 additions & 18 deletions scripts/utils/generateMain.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ const dedent = require('dedent')
const deepmerge = require('deepmerge')
const { ndjsonApi } = require('./generateApis')

const supportedResponses = [
'Search',
'MSearch',
'Create',
'Index',
'Delete',
'Update',
'Get',
'Bulk'
]

const ndjsonApiKey = ndjsonApi
.map(api => {
return api
Expand Down Expand Up @@ -220,48 +231,49 @@ function buildMethodDefinition (opts, api, name, hasBody) {
const Name = toPascalCase(name)
const bodyType = ndjsonApiKey.includes(Name) ? 'RequestNDBody' : 'RequestBody'
const defaultBodyType = ndjsonApiKey.includes(Name) ? 'Record<string, any>[]' : 'Record<string, any>'
const responseType = supportedResponses.includes(Name) ? `Res.${Name}Response` : 'Record<string, any>'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: why not export responses used Response namespace?


if (opts.kibana) {
if (hasBody) {
return [
{ key: `${camelify(api)}<TResponse = Record<string, any>, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params?: RequestParams.${Name}<TRequestBody>, options?: TransportRequestOptions)`, val: `TransportRequestPromise<ApiResponse<TResponse, TContext>>` }
{ key: `${camelify(api)}<TResponse = Record<string, any>, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params?: Req.${Name}Request<TRequestBody>, options?: TransportRequestOptions)`, val: `TransportRequestPromise<ApiResponse<TResponse, TContext>>` }
]
} else {
return [
{ key: `${camelify(api)}<TResponse = Record<string, any>, TContext = Context>(params?: RequestParams.${Name}, options?: TransportRequestOptions)`, val: `TransportRequestPromise<ApiResponse<TResponse, TContext>>` }
{ key: `${camelify(api)}<TResponse = Record<string, any>, TContext = Context>(params?: Req.${Name}Request, options?: TransportRequestOptions)`, val: `TransportRequestPromise<ApiResponse<TResponse, TContext>>` }
]
}
}

if (hasBody) {
let methods = [
{ key: `${api}<TResponse = Record<string, any>, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params?: RequestParams.${Name}<TRequestBody>, options?: TransportRequestOptions)`, val: `TransportRequestPromise<ApiResponse<TResponse, TContext>>` },
{ key: `${api}<TResponse = Record<string, any>, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${api}<TResponse = Record<string, any>, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params: RequestParams.${Name}<TRequestBody>, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${api}<TResponse = Record<string, any>, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params: RequestParams.${Name}<TRequestBody>, options: TransportRequestOptions, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` }
{ key: `${api}<TResponse = ${responseType}, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params?: Req.${Name}Request<TRequestBody>, options?: TransportRequestOptions)`, val: `TransportRequestPromise<ApiResponse<TResponse, TContext>>` },
{ key: `${api}<TResponse = ${responseType}, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${api}<TResponse = ${responseType}, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params: Req.${Name}Request<TRequestBody>, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${api}<TResponse = ${responseType}, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params: Req.${Name}Request<TRequestBody>, options: TransportRequestOptions, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` }
]
if (isSnakeCased(api)) {
methods = methods.concat([
{ key: `${camelify(api)}<TResponse = Record<string, any>, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params?: RequestParams.${Name}<TRequestBody>, options?: TransportRequestOptions)`, val: `TransportRequestPromise<ApiResponse<TResponse, TContext>>` },
{ key: `${camelify(api)}<TResponse = Record<string, any>, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${camelify(api)}<TResponse = Record<string, any>, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params: RequestParams.${Name}<TRequestBody>, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${camelify(api)}<TResponse = Record<string, any>, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params: RequestParams.${Name}<TRequestBody>, options: TransportRequestOptions, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` }
{ key: `${camelify(api)}<TResponse = ${responseType}, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params?: Req.${Name}Request<TRequestBody>, options?: TransportRequestOptions)`, val: `TransportRequestPromise<ApiResponse<TResponse, TContext>>` },
{ key: `${camelify(api)}<TResponse = ${responseType}, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${camelify(api)}<TResponse = ${responseType}, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params: Req.${Name}Request<TRequestBody>, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${camelify(api)}<TResponse = ${responseType}, TRequestBody extends ${bodyType} = ${defaultBodyType}, TContext = Context>(params: Req.${Name}Request<TRequestBody>, options: TransportRequestOptions, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` }
])
}
return methods
} else {
let methods = [
{ key: `${api}<TResponse = Record<string, any>, TContext = Context>(params?: RequestParams.${Name}, options?: TransportRequestOptions)`, val: `TransportRequestPromise<ApiResponse<TResponse, TContext>>` },
{ key: `${api}<TResponse = Record<string, any>, TContext = Context>(callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${api}<TResponse = Record<string, any>, TContext = Context>(params: RequestParams.${Name}, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${api}<TResponse = Record<string, any>, TContext = Context>(params: RequestParams.${Name}, options: TransportRequestOptions, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` }
{ key: `${api}<TResponse = ${responseType}, TContext = Context>(params?: Req.${Name}Request, options?: TransportRequestOptions)`, val: `TransportRequestPromise<ApiResponse<TResponse, TContext>>` },
{ key: `${api}<TResponse = ${responseType}, TContext = Context>(callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${api}<TResponse = ${responseType}, TContext = Context>(params: Req.${Name}Request, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${api}<TResponse = ${responseType}, TContext = Context>(params: Req.${Name}Request, options: TransportRequestOptions, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` }
]
if (isSnakeCased(api)) {
methods = methods.concat([
{ key: `${camelify(api)}<TResponse = Record<string, any>, TContext = Context>(params?: RequestParams.${Name}, options?: TransportRequestOptions)`, val: `TransportRequestPromise<ApiResponse<TResponse, TContext>>` },
{ key: `${camelify(api)}<TResponse = Record<string, any>, TContext = Context>(callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${camelify(api)}<TResponse = Record<string, any>, TContext = Context>(params: RequestParams.${Name}, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${camelify(api)}<TResponse = Record<string, any>, TContext = Context>(params: RequestParams.${Name}, options: TransportRequestOptions, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` }
{ key: `${camelify(api)}<TResponse = ${responseType}, TContext = Context>(params?: Req.${Name}Request, options?: TransportRequestOptions)`, val: `TransportRequestPromise<ApiResponse<TResponse, TContext>>` },
{ key: `${camelify(api)}<TResponse = ${responseType}, TContext = Context>(callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${camelify(api)}<TResponse = ${responseType}, TContext = Context>(params: Req.${Name}Request, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` },
{ key: `${camelify(api)}<TResponse = ${responseType}, TContext = Context>(params: Req.${Name}Request, options: TransportRequestOptions, callback: callbackFn<TResponse, TContext>)`, val: `TransportRequestCallback` }
])
}
return methods
Expand Down
22 changes: 20 additions & 2 deletions scripts/utils/generateRequestTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ const ndjsonApiKey = ndjsonApi
.map(toPascalCase)

function generate (version, api) {
var exportNewLineCount = 0
const release = semver.valid(version) ? semver.major(version) : version
var types = `// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

import { RequestBody, RequestNDBody } from '../lib/Transport'

export interface Generic {
interface GenericRequest {
method?: string;
filter_path?: string | string[];
pretty?: boolean;
Expand All @@ -35,6 +36,11 @@ export interface Generic {
`

api.forEach(generateRequestType)
types += '\nexport {\n'
api.forEach(generateExport)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. the line above and the line below belong to generateExport operation.
  2. IMO it's not obvious that generateRequestType & generateExport mutate types variable. I'd refactor the function to follow Transform pattern

// removes last comma
types = types.slice(0, -2) + '\n'
types += '}'
return types

function generateRequestType (spec) {
Expand Down Expand Up @@ -96,7 +102,7 @@ export interface Generic {
const bodyGeneric = ndjsonApiKey.includes(toPascalCase(name)) ? 'RequestNDBody' : 'RequestBody'

const code = `
export interface ${toPascalCase(name)}${body ? `<T = ${bodyGeneric}>` : ''} extends Generic {
interface ${toPascalCase(name)}Request${body ? `<T = ${bodyGeneric}>` : ''} extends GenericRequest {
${partsArr.map(genLine).join('\n ')}
${paramsArr.map(genLine).join('\n ')}
${body ? `body${body.required ? '' : '?'}: T;` : ''}
Expand Down Expand Up @@ -152,6 +158,18 @@ export interface ${toPascalCase(name)}${body ? `<T = ${bodyGeneric}>` : ''} exte
return type
}
}

function generateExport (spec) {
const api = Object.keys(spec)[0]
const name = api
.replace(/\.([a-z])/g, k => k[1].toUpperCase())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We repeat the same operation across several generate utils. I'm wondering if we can normalize data once in a centralized manner.

.replace(/_([a-z])/g, k => k[1].toUpperCase())
types += exportNewLineCount === 0 ? ' ' : ''
types += toPascalCase(name) + 'Request,'
types += exportNewLineCount < 3 ? ' ' : '\n'
exportNewLineCount += 1
if (exportNewLineCount > 3) exportNewLineCount = 0
}
}

function intersect (first, ...rest) {
Expand Down
Loading