@readme/api-core 7.0.0-alpha.3 → 7.0.0-alpha.4

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/dist/index.js CHANGED
@@ -1,96 +1,515 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.prepareServer = exports.prepareParams = exports.prepareAuth = exports.parseResponse = exports.getJSONSchemaDefaults = void 0;
7
- const oas_to_har_1 = __importDefault(require("@readme/oas-to-har"));
8
- const fetch_har_1 = __importDefault(require("fetch-har"));
9
- const fetchError_1 = __importDefault(require("./errors/fetchError"));
10
- const getJSONSchemaDefaults_1 = __importDefault(require("./lib/getJSONSchemaDefaults"));
11
- exports.getJSONSchemaDefaults = getJSONSchemaDefaults_1.default;
12
- const parseResponse_1 = __importDefault(require("./lib/parseResponse"));
13
- exports.parseResponse = parseResponse_1.default;
14
- const prepareAuth_1 = __importDefault(require("./lib/prepareAuth"));
15
- exports.prepareAuth = prepareAuth_1.default;
16
- const prepareParams_1 = __importDefault(require("./lib/prepareParams"));
17
- exports.prepareParams = prepareParams_1.default;
18
- const prepareServer_1 = __importDefault(require("./lib/prepareServer"));
19
- exports.prepareServer = prepareServer_1.default;
20
- class APICore {
21
- spec;
22
- auth = [];
23
- server = false;
24
- config = {};
25
- userAgent;
26
- constructor(spec, userAgent) {
27
- if (spec)
28
- this.spec = spec;
29
- if (userAgent)
30
- this.userAgent = userAgent;
1
+ import {
2
+ FetchError
3
+ } from "./chunk-L7DN22AA.js";
4
+
5
+ // src/index.ts
6
+ import oasToHar from "@readme/oas-to-har";
7
+ import fetchHar from "fetch-har";
8
+
9
+ // src/lib/getJSONSchemaDefaults.ts
10
+ import traverse from "json-schema-traverse";
11
+ function getJSONSchemaDefaults(jsonSchemas) {
12
+ return jsonSchemas.map(({ type: payloadType, schema: jsonSchema }) => {
13
+ const defaults = {};
14
+ traverse(
15
+ jsonSchema,
16
+ (schema, pointer, rootSchema, parentPointer, parentKeyword, parentSchema, indexProperty) => {
17
+ if (!pointer.startsWith("/properties/")) {
18
+ return;
19
+ }
20
+ if (Array.isArray(parentSchema?.required) && parentSchema?.required.includes(String(indexProperty))) {
21
+ if (schema.type === "object" && indexProperty) {
22
+ defaults[indexProperty] = {};
23
+ }
24
+ let destination = defaults;
25
+ if (parentPointer) {
26
+ parentPointer.replace(/\/properties/g, "").split("/").forEach((subSchema) => {
27
+ if (subSchema === "") {
28
+ return;
29
+ }
30
+ destination = destination?.[subSchema] || {};
31
+ });
32
+ }
33
+ if (schema.default !== void 0) {
34
+ if (indexProperty !== void 0) {
35
+ destination[indexProperty] = schema.default;
36
+ }
37
+ }
38
+ }
39
+ }
40
+ );
41
+ if (!Object.keys(defaults).length) {
42
+ return {};
31
43
  }
32
- setSpec(spec) {
33
- this.spec = spec;
44
+ return {
45
+ // @todo should we filter out empty and undefined objects from here with `remove-undefined-objects`?
46
+ [payloadType]: defaults
47
+ };
48
+ }).reduce((prev, next) => Object.assign(prev, next));
49
+ }
50
+
51
+ // src/lib/parseResponse.ts
52
+ import { matchesMimeType } from "oas/utils";
53
+ async function parseResponse(response) {
54
+ const contentType = response.headers.get("Content-Type");
55
+ const isJSON = contentType && (matchesMimeType.json(contentType) || matchesMimeType.wildcard(contentType));
56
+ const responseBody = await response.text();
57
+ let data = responseBody;
58
+ if (isJSON) {
59
+ try {
60
+ data = JSON.parse(responseBody);
61
+ } catch (e) {
34
62
  }
35
- setConfig(config) {
36
- this.config = config;
37
- return this;
63
+ }
64
+ return {
65
+ data,
66
+ status: response.status,
67
+ headers: response.headers,
68
+ res: response
69
+ };
70
+ }
71
+
72
+ // src/lib/prepareAuth.ts
73
+ function prepareAuth(authKey, operation) {
74
+ if (authKey.length === 0) {
75
+ return {};
76
+ }
77
+ const preparedAuth = {};
78
+ const security = operation.getSecurity();
79
+ if (security.length === 0) {
80
+ return {};
81
+ }
82
+ if (security.every((s) => Object.keys(s).length > 1)) {
83
+ throw new Error(
84
+ "Sorry, this operation currently requires multiple forms of authentication which this library doesn't yet support."
85
+ );
86
+ }
87
+ const usableSecurity = security.map((s) => {
88
+ return Object.keys(s).length === 1 ? s : false;
89
+ }).filter(Boolean);
90
+ const usableSecuritySchemes = usableSecurity.map((s) => Object.keys(s)).reduce((prev, next) => prev.concat(next), []);
91
+ const preparedSecurity = operation.prepareSecurity();
92
+ if (authKey.length >= 2) {
93
+ if (!("Basic" in preparedSecurity)) {
94
+ throw new Error("Multiple auth tokens were supplied for this endpoint but only a single token is needed.");
95
+ }
96
+ const schemes2 = preparedSecurity.Basic.filter((s) => usableSecuritySchemes.includes(s._key));
97
+ if (!schemes2.length) {
98
+ throw new Error(
99
+ "Credentials for Basic Authentication were supplied but this operation requires another form of auth in that case, which this library does not yet support. This operation does, however, allow supplying a single auth token."
100
+ );
38
101
  }
39
- setUserAgent(userAgent) {
40
- this.userAgent = userAgent;
41
- return this;
102
+ const scheme2 = schemes2.shift();
103
+ preparedAuth[scheme2._key] = {
104
+ user: authKey[0],
105
+ pass: authKey.length === 2 ? authKey[1] : ""
106
+ };
107
+ return preparedAuth;
108
+ }
109
+ const usableScheme = usableSecuritySchemes[0];
110
+ const schemes = Object.entries(preparedSecurity).map(([, ps]) => ps.filter((s) => usableScheme === s._key)).reduce((prev, next) => prev.concat(next), []);
111
+ const scheme = schemes.shift();
112
+ switch (scheme.type) {
113
+ case "http":
114
+ if (scheme.scheme === "basic") {
115
+ preparedAuth[scheme._key] = {
116
+ user: authKey[0],
117
+ pass: authKey.length === 2 ? authKey[1] : ""
118
+ };
119
+ } else if (scheme.scheme === "bearer") {
120
+ preparedAuth[scheme._key] = authKey[0];
121
+ }
122
+ break;
123
+ case "oauth2":
124
+ preparedAuth[scheme._key] = authKey[0];
125
+ break;
126
+ case "apiKey":
127
+ if (scheme.in === "query" || scheme.in === "header" || scheme.in === "cookie") {
128
+ preparedAuth[scheme._key] = authKey[0];
129
+ }
130
+ break;
131
+ default:
132
+ throw new Error(
133
+ `Sorry, this API currently uses a security scheme, ${scheme.type}, which this library doesn't yet support.`
134
+ );
135
+ }
136
+ return preparedAuth;
137
+ }
138
+
139
+ // src/lib/prepareParams.ts
140
+ import fs from "fs";
141
+ import path from "path";
142
+ import stream from "stream";
143
+ import caseless from "caseless";
144
+ import DatauriParser from "datauri/parser.js";
145
+ import datauri from "datauri/sync.js";
146
+ import getStream from "get-stream";
147
+ import lodashMerge from "lodash.merge";
148
+ import removeUndefinedObjects from "remove-undefined-objects";
149
+ var specialHeaders = ["accept", "authorization"];
150
+ function digestParameters(parameters) {
151
+ return parameters.reduce((prev, param) => {
152
+ if ("$ref" in param || "allOf" in param || "anyOf" in param || "oneOf" in param) {
153
+ throw new Error("The OpenAPI document for this operation wasn't dereferenced before processing.");
154
+ } else if (param.name in prev) {
155
+ throw new Error(
156
+ `The operation you are using has the same parameter, ${param.name}, spread across multiple entry points. We unfortunately can't handle this right now.`
157
+ );
158
+ }
159
+ return Object.assign(prev, { [param.name]: param });
160
+ }, {});
161
+ }
162
+ function isEmpty(obj) {
163
+ return [Object, Array].includes((obj || {}).constructor) && !Object.entries(obj || {}).length;
164
+ }
165
+ function isObject(thing) {
166
+ if (thing instanceof stream.Readable) {
167
+ return false;
168
+ }
169
+ return typeof thing === "object" && thing !== null && !Array.isArray(thing);
170
+ }
171
+ function isPrimitive(obj) {
172
+ return obj === null || typeof obj === "number" || typeof obj === "string";
173
+ }
174
+ function merge(src, target) {
175
+ if (Array.isArray(target)) {
176
+ return target;
177
+ } else if (!isObject(target)) {
178
+ return target;
179
+ }
180
+ return lodashMerge(src, target);
181
+ }
182
+ function processFile(paramName, file) {
183
+ if (typeof file === "string") {
184
+ const resolvedFile = path.resolve(file);
185
+ return new Promise((resolve, reject) => {
186
+ fs.stat(resolvedFile, async (err) => {
187
+ if (err) {
188
+ if (err.code === "ENOENT") {
189
+ return resolve(void 0);
190
+ }
191
+ return reject(err);
192
+ }
193
+ const fileMetadata = await datauri(resolvedFile);
194
+ const payloadFilename = encodeURIComponent(path.basename(resolvedFile));
195
+ return resolve({
196
+ paramName,
197
+ base64: fileMetadata?.content?.replace(";base64", `;name=${payloadFilename};base64`),
198
+ filename: payloadFilename,
199
+ buffer: fileMetadata.buffer
200
+ });
201
+ });
202
+ });
203
+ } else if (file instanceof stream.Readable) {
204
+ return getStream.buffer(file).then((buffer) => {
205
+ const filePath = file.path;
206
+ const parser = new DatauriParser();
207
+ const base64 = parser.format(filePath, buffer).content;
208
+ const payloadFilename = encodeURIComponent(path.basename(filePath));
209
+ return {
210
+ paramName,
211
+ base64: base64?.replace(";base64", `;name=${payloadFilename};base64`),
212
+ filename: payloadFilename,
213
+ buffer
214
+ };
215
+ });
216
+ }
217
+ return Promise.reject(
218
+ new TypeError(
219
+ paramName ? `The data supplied for the \`${paramName}\` request body parameter is not a file handler that we support.` : "The data supplied for the request body payload is not a file handler that we support."
220
+ )
221
+ );
222
+ }
223
+ async function prepareParams(operation, body, metadata) {
224
+ let metadataIntersected = false;
225
+ const digestedParameters = digestParameters(operation.getParameters());
226
+ const jsonSchema = operation.getParametersAsJSONSchema();
227
+ metadata = removeUndefinedObjects(metadata);
228
+ if (!jsonSchema && (body !== void 0 || metadata !== void 0)) {
229
+ let throwNoParamsError = true;
230
+ if (body !== void 0) {
231
+ if (typeof body === "object" && body !== null && !Array.isArray(body)) {
232
+ if (Object.keys(body).length <= 2) {
233
+ const bodyParams = caseless(body);
234
+ if (specialHeaders.some((header) => bodyParams.has(header))) {
235
+ throwNoParamsError = false;
236
+ }
237
+ }
238
+ }
42
239
  }
43
- setAuth(...values) {
44
- this.auth = values;
45
- return this;
240
+ if (throwNoParamsError) {
241
+ throw new Error(
242
+ "You supplied metadata and/or body data for this operation but it doesn't have any documented parameters or request payloads. If you think this is an error please contact support for the API you're using."
243
+ );
46
244
  }
47
- setServer(url, variables = {}) {
48
- this.server = { url, variables };
49
- return this;
245
+ }
246
+ const jsonSchemaDefaults = jsonSchema ? getJSONSchemaDefaults(jsonSchema) : {};
247
+ const params = jsonSchemaDefaults;
248
+ if (typeof body !== "undefined") {
249
+ if (Array.isArray(body) || isPrimitive(body)) {
250
+ params.body = merge(params.body, body);
251
+ } else if (typeof metadata === "undefined") {
252
+ const headerParams = caseless({});
253
+ Object.entries(digestedParameters).forEach(([paramName, param]) => {
254
+ if (param.in === "header") {
255
+ headerParams.set(paramName, "");
256
+ }
257
+ });
258
+ specialHeaders.forEach((header) => {
259
+ if (!headerParams.has(header)) {
260
+ headerParams.set(header, "");
261
+ }
262
+ });
263
+ const intersection = Object.keys(body).filter((value) => {
264
+ if (Object.keys(digestedParameters).includes(value)) {
265
+ return true;
266
+ } else if (headerParams.has(value)) {
267
+ return true;
268
+ }
269
+ return false;
270
+ }).length;
271
+ if (intersection && intersection / Object.keys(body).length > 0.25) {
272
+ metadataIntersected = true;
273
+ metadata = merge(params.body, body);
274
+ body = void 0;
275
+ } else {
276
+ params.body = merge(params.body, body);
277
+ }
278
+ } else {
279
+ params.body = merge(params.body, body);
50
280
  }
51
- async fetch(path, method, body, metadata) {
52
- const operation = this.spec.operation(path, method);
53
- return this.fetchOperation(operation, body, metadata);
281
+ }
282
+ if (!operation.hasRequestBody()) {
283
+ delete params.body;
284
+ } else {
285
+ if (!("body" in params))
286
+ params.body = {};
287
+ const payloadJsonSchema = jsonSchema.find((js) => js.type === "body");
288
+ if (payloadJsonSchema) {
289
+ if (!params.files)
290
+ params.files = {};
291
+ const conversions = [];
292
+ if (payloadJsonSchema.schema?.properties) {
293
+ Object.entries(payloadJsonSchema.schema?.properties).filter(([, schema]) => schema?.format === "binary").filter(([prop]) => Object.keys(params.body).includes(prop)).forEach(([prop]) => {
294
+ conversions.push(processFile(prop, params.body[prop]));
295
+ });
296
+ } else if (payloadJsonSchema.schema?.type === "string") {
297
+ if (payloadJsonSchema.schema?.format === "binary") {
298
+ conversions.push(processFile(void 0, params.body));
299
+ }
300
+ }
301
+ await Promise.all(conversions).then((fileMetadata) => fileMetadata.filter(Boolean)).then((fm) => {
302
+ fm.forEach((fileMetadata) => {
303
+ if (!fileMetadata) {
304
+ return;
305
+ }
306
+ if (fileMetadata.paramName) {
307
+ params.body[fileMetadata.paramName] = fileMetadata.base64;
308
+ } else {
309
+ params.body = fileMetadata.base64;
310
+ }
311
+ if (fileMetadata.buffer && params?.files) {
312
+ params.files[fileMetadata.filename] = fileMetadata.buffer;
313
+ }
314
+ });
315
+ });
54
316
  }
55
- async fetchOperation(operation, body, metadata) {
56
- return (0, prepareParams_1.default)(operation, body, metadata).then(params => {
57
- const data = { ...params };
58
- // If `sdk.server()` has been issued data then we need to do some extra work to figure out
59
- // how to use that supplied server, and also handle any server variables that were sent
60
- // alongside it.
61
- if (this.server) {
62
- const preparedServer = (0, prepareServer_1.default)(this.spec, this.server.url, this.server.variables);
63
- if (preparedServer) {
64
- data.server = preparedServer;
65
- }
317
+ }
318
+ if (operation.isFormUrlEncoded()) {
319
+ params.formData = merge(params.formData, params.body);
320
+ delete params.body;
321
+ }
322
+ if (typeof metadata !== "undefined") {
323
+ if (!("cookie" in params))
324
+ params.cookie = {};
325
+ if (!("header" in params))
326
+ params.header = {};
327
+ if (!("path" in params))
328
+ params.path = {};
329
+ if (!("query" in params))
330
+ params.query = {};
331
+ Object.entries(digestedParameters).forEach(([paramName, param]) => {
332
+ let value;
333
+ let metadataHeaderParam;
334
+ if (typeof metadata === "object" && !isEmpty(metadata)) {
335
+ if (paramName in metadata) {
336
+ value = metadata[paramName];
337
+ metadataHeaderParam = paramName;
338
+ } else if (param.in === "header") {
339
+ metadataHeaderParam = Object.keys(metadata).find((k) => k.toLowerCase() === paramName.toLowerCase()) || "";
340
+ value = metadata[metadataHeaderParam];
341
+ }
342
+ }
343
+ if (value === void 0) {
344
+ return;
345
+ }
346
+ switch (param.in) {
347
+ case "path":
348
+ params.path[paramName] = value;
349
+ if (metadata?.[paramName])
350
+ delete metadata[paramName];
351
+ break;
352
+ case "query":
353
+ params.query[paramName] = value;
354
+ if (metadata?.[paramName])
355
+ delete metadata[paramName];
356
+ break;
357
+ case "header":
358
+ params.header[paramName.toLowerCase()] = value;
359
+ if (metadataHeaderParam && metadata?.[metadataHeaderParam])
360
+ delete metadata[metadataHeaderParam];
361
+ break;
362
+ case "cookie":
363
+ params.cookie[paramName] = value;
364
+ if (metadata?.[paramName])
365
+ delete metadata[paramName];
366
+ break;
367
+ default:
368
+ }
369
+ if (metadataIntersected && operation.isFormUrlEncoded()) {
370
+ if (paramName in params.formData) {
371
+ delete params.formData[paramName];
372
+ }
373
+ }
374
+ });
375
+ if (!isEmpty(metadata)) {
376
+ if (typeof metadata === "object") {
377
+ specialHeaders.forEach((headerName) => {
378
+ const headerParam = Object.keys(metadata || {}).find((m) => m.toLowerCase() === headerName);
379
+ if (headerParam) {
380
+ if (typeof metadata === "object") {
381
+ if (typeof params.header === "object") {
382
+ params.header[headerName] = metadata[headerParam];
383
+ }
384
+ delete metadata[headerParam];
66
385
  }
67
- // @ts-expect-error `this.auth` typing is off. FIXME
68
- const har = (0, oas_to_har_1.default)(this.spec, operation, data, (0, prepareAuth_1.default)(this.auth, operation));
69
- let timeoutSignal;
70
- const init = {};
71
- if (this.config.timeout) {
72
- const controller = new AbortController();
73
- timeoutSignal = setTimeout(() => controller.abort(), this.config.timeout);
74
- init.signal = controller.signal;
75
- }
76
- return (0, fetch_har_1.default)(har, {
77
- files: data.files || {},
78
- init,
79
- userAgent: this.userAgent,
80
- })
81
- .then(async (res) => {
82
- const parsed = await (0, parseResponse_1.default)(res);
83
- if (res.status >= 400 && res.status <= 599) {
84
- throw new fetchError_1.default(parsed.status, parsed.data, parsed.headers, parsed.res);
85
- }
86
- return parsed;
87
- })
88
- .finally(() => {
89
- if (this.config.timeout) {
90
- clearTimeout(timeoutSignal);
91
- }
92
- });
386
+ }
93
387
  });
388
+ }
389
+ if (operation.isFormUrlEncoded()) {
390
+ params.formData = merge(params.formData, metadata);
391
+ } else {
392
+ }
393
+ }
394
+ }
395
+ ["body", "cookie", "files", "formData", "header", "path", "query"].forEach((type) => {
396
+ if (type in params && isEmpty(params[type])) {
397
+ delete params[type];
94
398
  }
399
+ });
400
+ return params;
95
401
  }
96
- exports.default = APICore;
402
+
403
+ // src/lib/prepareServer.ts
404
+ function stripTrailingSlash(url) {
405
+ if (url[url.length - 1] === "/") {
406
+ return url.slice(0, -1);
407
+ }
408
+ return url;
409
+ }
410
+ function prepareServer(spec, url, variables = {}) {
411
+ let serverIdx;
412
+ const sanitizedUrl = stripTrailingSlash(url);
413
+ (spec.api.servers || []).forEach((server, i) => {
414
+ if (server.url === sanitizedUrl) {
415
+ serverIdx = i;
416
+ }
417
+ });
418
+ if (serverIdx) {
419
+ return {
420
+ selected: serverIdx,
421
+ variables
422
+ };
423
+ } else if (Object.keys(variables).length) {
424
+ } else {
425
+ const server = spec.splitVariables(url);
426
+ if (server) {
427
+ return {
428
+ selected: server.selected,
429
+ variables: server.variables
430
+ };
431
+ }
432
+ }
433
+ return false;
434
+ }
435
+
436
+ // src/index.ts
437
+ var APICore = class {
438
+ spec;
439
+ auth = [];
440
+ server = false;
441
+ config = {};
442
+ userAgent;
443
+ constructor(spec, userAgent) {
444
+ if (spec)
445
+ this.spec = spec;
446
+ if (userAgent)
447
+ this.userAgent = userAgent;
448
+ }
449
+ setSpec(spec) {
450
+ this.spec = spec;
451
+ }
452
+ setConfig(config) {
453
+ this.config = config;
454
+ return this;
455
+ }
456
+ setUserAgent(userAgent) {
457
+ this.userAgent = userAgent;
458
+ return this;
459
+ }
460
+ setAuth(...values) {
461
+ this.auth = values;
462
+ return this;
463
+ }
464
+ setServer(url, variables = {}) {
465
+ this.server = { url, variables };
466
+ return this;
467
+ }
468
+ async fetch(path2, method, body, metadata) {
469
+ const operation = this.spec.operation(path2, method);
470
+ return this.fetchOperation(operation, body, metadata);
471
+ }
472
+ async fetchOperation(operation, body, metadata) {
473
+ return prepareParams(operation, body, metadata).then((params) => {
474
+ const data = { ...params };
475
+ if (this.server) {
476
+ const preparedServer = prepareServer(this.spec, this.server.url, this.server.variables);
477
+ if (preparedServer) {
478
+ data.server = preparedServer;
479
+ }
480
+ }
481
+ const har = oasToHar(this.spec, operation, data, prepareAuth(this.auth, operation));
482
+ let timeoutSignal;
483
+ const init = {};
484
+ if (this.config.timeout) {
485
+ const controller = new AbortController();
486
+ timeoutSignal = setTimeout(() => controller.abort(), this.config.timeout);
487
+ init.signal = controller.signal;
488
+ }
489
+ return fetchHar(har, {
490
+ files: data.files || {},
491
+ init,
492
+ userAgent: this.userAgent
493
+ }).then(async (res) => {
494
+ const parsed = await parseResponse(res);
495
+ if (res.status >= 400 && res.status <= 599) {
496
+ throw new FetchError(
497
+ parsed.status,
498
+ parsed.data,
499
+ parsed.headers,
500
+ parsed.res
501
+ );
502
+ }
503
+ return parsed;
504
+ }).finally(() => {
505
+ if (this.config.timeout) {
506
+ clearTimeout(timeoutSignal);
507
+ }
508
+ });
509
+ });
510
+ }
511
+ };
512
+ export {
513
+ APICore as default
514
+ };
515
+ //# sourceMappingURL=index.js.map