httpsnippet-client-api 7.0.0-alpha.6 → 7.0.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -9
- package/dist/index.cjs +253 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -6
- package/dist/index.d.ts +20 -6
- package/dist/index.js +249 -21
- package/dist/index.js.map +1 -1
- package/package.json +12 -10
- package/src/index.ts +96 -29
- package/tsup.config.ts +11 -1
package/src/index.ts
CHANGED
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
import type { ReducedHelperObject } from '@readme/httpsnippet/helpers/reducer';
|
|
2
|
-
import type { Client } from '@readme/httpsnippet/targets';
|
|
3
|
-
import type Operation from 'oas/operation';
|
|
4
|
-
import type { HttpMethods, OASDocument } from 'oas/
|
|
2
|
+
import type { Client, ClientPlugin } from '@readme/httpsnippet/targets';
|
|
3
|
+
import type { Operation } from 'oas/operation';
|
|
4
|
+
import type { HttpMethods, OASDocument } from 'oas/types';
|
|
5
5
|
|
|
6
6
|
import { CodeBuilder } from '@readme/httpsnippet/helpers/code-builder';
|
|
7
|
+
import camelCase from 'camelcase'; // eslint-disable-line import/no-extraneous-dependencies
|
|
7
8
|
import contentType from 'content-type';
|
|
8
9
|
import Oas from 'oas';
|
|
9
10
|
import { matchesMimeType } from 'oas/utils';
|
|
10
|
-
import
|
|
11
|
+
import { isReservedOrBuiltinsLC } from 'reserved2';
|
|
12
|
+
import stringifyObject from 'stringify-object'; // eslint-disable-line import/no-extraneous-dependencies
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @note This regex also exists in `api/fetcher`.
|
|
16
|
+
*
|
|
17
|
+
* @example @petstore/v1.0#n6kvf10vakpemvplx
|
|
18
|
+
* @example @petstore#n6kvf10vakpemvplx
|
|
19
|
+
*/
|
|
20
|
+
const registryUUIDRegex = /^@(?<project>[a-zA-Z0-9-_]+)(\/?(?<version>.+))?#(?<uuid>[a-z0-9]+)$/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @note This function also exists in `api/fetcher`.
|
|
24
|
+
*/
|
|
25
|
+
function getProjectPrefixFromRegistryUUID(uri: string) {
|
|
26
|
+
const matches = uri.match(registryUUIDRegex);
|
|
27
|
+
if (!matches) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return matches.groups?.project;
|
|
32
|
+
}
|
|
11
33
|
|
|
12
34
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
35
|
function stringify(obj: any, opts = {}) {
|
|
14
36
|
return stringifyObject(obj, { indent: ' ', ...opts });
|
|
15
37
|
}
|
|
16
38
|
|
|
17
|
-
function buildAuthSnippet(authKey: string | string
|
|
39
|
+
function buildAuthSnippet(sdkVariable: string, authKey: string[] | string) {
|
|
18
40
|
// Auth key will be an array for Basic auth cases.
|
|
19
41
|
if (Array.isArray(authKey)) {
|
|
20
42
|
const auth: string[] = [];
|
|
@@ -27,10 +49,10 @@ function buildAuthSnippet(authKey: string | string[]) {
|
|
|
27
49
|
auth.push(`'${token.replace(/'/g, "\\'")}'`);
|
|
28
50
|
});
|
|
29
51
|
|
|
30
|
-
return
|
|
52
|
+
return `${sdkVariable}.auth(${auth.join(', ')});`;
|
|
31
53
|
}
|
|
32
54
|
|
|
33
|
-
return
|
|
55
|
+
return `${sdkVariable}.auth('${authKey.replace(/'/g, "\\'")}');`;
|
|
34
56
|
}
|
|
35
57
|
|
|
36
58
|
function getAuthSources(operation: Operation) {
|
|
@@ -75,8 +97,24 @@ function getAuthSources(operation: Operation) {
|
|
|
75
97
|
}
|
|
76
98
|
|
|
77
99
|
interface APIOptions {
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
api?: {
|
|
101
|
+
definition: OASDocument;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The string to identify this SDK as. This is used in the `import sdk from '<identifier>'`
|
|
105
|
+
* sample as well as the the variable name we attach the SDK to.
|
|
106
|
+
*
|
|
107
|
+
* @example `@api/developers`
|
|
108
|
+
*/
|
|
109
|
+
identifier?: string;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The URI that is used to download this API definition from `npx api install`.
|
|
113
|
+
*
|
|
114
|
+
* @example `@developers/v2.0#17273l2glm9fq4l5`
|
|
115
|
+
*/
|
|
116
|
+
registryURI: string;
|
|
117
|
+
};
|
|
80
118
|
escapeBrackets?: boolean;
|
|
81
119
|
indent?: string | false;
|
|
82
120
|
}
|
|
@@ -88,28 +126,47 @@ const client: Client<APIOptions> = {
|
|
|
88
126
|
link: 'https://npm.im/api',
|
|
89
127
|
description: 'Automatic SDK generation from an OpenAPI definition.',
|
|
90
128
|
extname: '.js',
|
|
129
|
+
installation: 'npx api install "{packageName}"',
|
|
91
130
|
},
|
|
92
131
|
convert: ({ cookiesObj, headersObj, postData, queryObj, url, ...source }, options) => {
|
|
93
132
|
const opts = {
|
|
94
133
|
...options,
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (!
|
|
98
|
-
throw new Error('This
|
|
99
|
-
} else if (!
|
|
100
|
-
throw new Error('This
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (!opts?.api) {
|
|
137
|
+
throw new Error('This HTTPSnippet client must have an `api` config supplied to it.');
|
|
138
|
+
} else if (!opts?.api?.definition) {
|
|
139
|
+
throw new Error('This HTTPSnippet client must have an `api.definition` option supplied to it.');
|
|
140
|
+
} else if (!opts?.api?.registryURI) {
|
|
141
|
+
throw new Error('This HTTPSnippet client must have an `api.registryURI` option supplied to it.');
|
|
101
142
|
}
|
|
102
143
|
|
|
103
144
|
const method = source.method.toLowerCase() as HttpMethods;
|
|
104
|
-
const oas = new Oas(opts.
|
|
145
|
+
const oas = new Oas(opts.api.definition);
|
|
105
146
|
const apiDefinition = oas.getDefinition();
|
|
106
147
|
const foundOperation = oas.findOperation(url, method);
|
|
107
148
|
if (!foundOperation) {
|
|
108
149
|
throw new Error(
|
|
109
|
-
`Unable to locate a matching operation in the supplied \`
|
|
150
|
+
`Unable to locate a matching operation in the supplied \`api.definition\` for: ${source.method} ${url}`,
|
|
110
151
|
);
|
|
111
152
|
}
|
|
112
153
|
|
|
154
|
+
let sdkPackageName: string | undefined;
|
|
155
|
+
let sdkVariable: string;
|
|
156
|
+
if (opts.api.identifier) {
|
|
157
|
+
sdkPackageName = opts.api.identifier;
|
|
158
|
+
|
|
159
|
+
sdkVariable = camelCase(opts.api.identifier);
|
|
160
|
+
if (isReservedOrBuiltinsLC(sdkVariable)) {
|
|
161
|
+
// If this identifier is a reserved JS word then we should prefix it with an underscore so
|
|
162
|
+
// this snippet can be valid code.
|
|
163
|
+
sdkVariable = `_${sdkVariable}`;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
sdkPackageName = getProjectPrefixFromRegistryUUID(opts.api.registryURI);
|
|
167
|
+
sdkVariable = camelCase(sdkPackageName || 'sdk');
|
|
168
|
+
}
|
|
169
|
+
|
|
113
170
|
const operationSlugs = foundOperation.url.slugs;
|
|
114
171
|
const operation = oas.operation(foundOperation.url.nonNormalizedPath, method);
|
|
115
172
|
const operationPathParameters = operation.getParameters().filter(param => param.in === 'path');
|
|
@@ -119,7 +176,7 @@ const client: Client<APIOptions> = {
|
|
|
119
176
|
|
|
120
177
|
const { blank, push, join } = new CodeBuilder({ indent: opts.indent || ' ' });
|
|
121
178
|
|
|
122
|
-
push(`
|
|
179
|
+
push(`import ${sdkVariable} from '@api/${sdkPackageName}';`);
|
|
123
180
|
blank();
|
|
124
181
|
|
|
125
182
|
// If we have multiple servers configured and our source URL differs from the stock URL that we
|
|
@@ -134,14 +191,14 @@ const client: Client<APIOptions> = {
|
|
|
134
191
|
const serverVars = oas.splitVariables(baseUrl);
|
|
135
192
|
const serverUrl = serverVars ? oas.url(serverVars.selected, serverVars.variables) : baseUrl;
|
|
136
193
|
|
|
137
|
-
configData.push(
|
|
194
|
+
configData.push(`${sdkVariable}.server('${serverUrl}');`);
|
|
138
195
|
}
|
|
139
196
|
}
|
|
140
197
|
|
|
141
|
-
let metadata: Record<string, string | string
|
|
198
|
+
let metadata: Record<string, string[] | string> = {};
|
|
142
199
|
Object.keys(queryObj).forEach(param => {
|
|
143
200
|
if (authSources.query.includes(param)) {
|
|
144
|
-
authData.push(buildAuthSnippet(queryObj[param]));
|
|
201
|
+
authData.push(buildAuthSnippet(sdkVariable, queryObj[param]));
|
|
145
202
|
|
|
146
203
|
// If this query param is part of an auth source then we don't want it doubled up in the
|
|
147
204
|
// snippet.
|
|
@@ -153,7 +210,7 @@ const client: Client<APIOptions> = {
|
|
|
153
210
|
|
|
154
211
|
Object.keys(cookiesObj).forEach(cookie => {
|
|
155
212
|
if (authSources.cookie.includes(cookie)) {
|
|
156
|
-
authData.push(buildAuthSnippet(cookiesObj[cookie]));
|
|
213
|
+
authData.push(buildAuthSnippet(sdkVariable, cookiesObj[cookie]));
|
|
157
214
|
|
|
158
215
|
// If this cookie is part of an auth source then we don't want it doubled up.
|
|
159
216
|
return;
|
|
@@ -199,7 +256,7 @@ const client: Client<APIOptions> = {
|
|
|
199
256
|
// into our auth data so we can build up an `.auth()` snippet for the SDK.
|
|
200
257
|
const authScheme = authSources.header[headerLower];
|
|
201
258
|
if (authScheme === '*') {
|
|
202
|
-
authData.push(buildAuthSnippet(headers[header]));
|
|
259
|
+
authData.push(buildAuthSnippet(sdkVariable, headers[header]));
|
|
203
260
|
} else {
|
|
204
261
|
// @ts-expect-error `headers[header]` is typed improperly in HTTPSnippet.
|
|
205
262
|
let authKey = headers[header].replace(`${authSources.header[headerLower]} `, '');
|
|
@@ -208,7 +265,7 @@ const client: Client<APIOptions> = {
|
|
|
208
265
|
authKey = authKey.split(':');
|
|
209
266
|
}
|
|
210
267
|
|
|
211
|
-
authData.push(buildAuthSnippet(authKey));
|
|
268
|
+
authData.push(buildAuthSnippet(sdkVariable, authKey));
|
|
212
269
|
}
|
|
213
270
|
|
|
214
271
|
delete headers[header];
|
|
@@ -232,9 +289,14 @@ const client: Client<APIOptions> = {
|
|
|
232
289
|
}
|
|
233
290
|
|
|
234
291
|
// If we haven't used our header anywhere else, or we've deleted it from the payload
|
|
235
|
-
// because it'll be handled internally by `api` then we should add
|
|
236
|
-
|
|
237
|
-
|
|
292
|
+
// because it'll be handled internally by `api` then we should add it into our code snippet.
|
|
293
|
+
if (['accept', 'content-type'].includes(headerLower)) {
|
|
294
|
+
requestHeaders[headerLower] = headers[header];
|
|
295
|
+
} else {
|
|
296
|
+
// Non-reserved headers retain their casing because we want to generate a snippet that
|
|
297
|
+
// matches the TS types that are created during codegeneration.
|
|
298
|
+
requestHeaders[header] = headers[header];
|
|
299
|
+
}
|
|
238
300
|
});
|
|
239
301
|
|
|
240
302
|
if (Object.keys(requestHeaders).length > 0) {
|
|
@@ -306,7 +368,7 @@ const client: Client<APIOptions> = {
|
|
|
306
368
|
push(configData.join('\n'));
|
|
307
369
|
}
|
|
308
370
|
|
|
309
|
-
push(
|
|
371
|
+
push(`${sdkVariable}.${accessor}(${args.join(', ')})`);
|
|
310
372
|
push('.then(({ data }) => console.log(data))', 1);
|
|
311
373
|
push('.catch(err => console.error(err));', 1);
|
|
312
374
|
|
|
@@ -314,4 +376,9 @@ const client: Client<APIOptions> = {
|
|
|
314
376
|
},
|
|
315
377
|
};
|
|
316
378
|
|
|
317
|
-
|
|
379
|
+
const plugin: ClientPlugin<APIOptions> = {
|
|
380
|
+
target: 'node',
|
|
381
|
+
client,
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
export default plugin;
|
package/tsup.config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
/* eslint-disable import/no-extraneous-dependencies, node/no-extraneous-import */
|
|
1
2
|
import type { Options } from 'tsup';
|
|
2
3
|
|
|
3
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
4
4
|
import { defineConfig } from 'tsup';
|
|
5
5
|
|
|
6
6
|
// eslint-disable-next-line import/no-relative-packages
|
|
@@ -11,5 +11,15 @@ export default defineConfig((options: Options) => ({
|
|
|
11
11
|
...config,
|
|
12
12
|
|
|
13
13
|
entry: ['src/index.ts'],
|
|
14
|
+
|
|
15
|
+
noExternal: [
|
|
16
|
+
// These dependencies are ESM-only but because we're building for ESM **and** CJS we can't
|
|
17
|
+
// treat them as external dependencies as CJS libraries can't load ESM code that uses `export`.
|
|
18
|
+
// `noExternal` will instead treeshake these dependencies down and include them in our compiled
|
|
19
|
+
// dists.
|
|
20
|
+
'camelcase',
|
|
21
|
+
'stringify-object',
|
|
22
|
+
],
|
|
23
|
+
|
|
14
24
|
silent: !options.watch,
|
|
15
25
|
}));
|