tempest-react-sdk 0.79.0 → 0.80.0
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/bin/lib/openapi/generate.hostile.test.mjs +45 -4
- package/bin/lib/openapi/generate.mjs +145 -34
- package/bin/lib/openapi/generate.test.mjs +203 -4
- package/bin/lib/openapi/schema-to-zod.mjs +4 -3
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +5 -1
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/build-url.cjs +1 -1
- package/dist/http/build-url.cjs.map +1 -1
- package/dist/http/build-url.js +3 -1
- package/dist/http/build-url.js.map +1 -1
- package/dist/http/presigned-post.cjs +2 -0
- package/dist/http/presigned-post.cjs.map +1 -0
- package/dist/http/presigned-post.js +73 -0
- package/dist/http/presigned-post.js.map +1 -0
- package/dist/sse/create-event-stream.cjs +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js +44 -30
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/sse/ticket-event-stream.cjs +2 -0
- package/dist/sse/ticket-event-stream.cjs.map +1 -0
- package/dist/sse/ticket-event-stream.js +41 -0
- package/dist/sse/ticket-event-stream.js.map +1 -0
- package/dist/sse/use-event-stream.cjs +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/sse/use-event-stream.js +7 -4
- package/dist/sse/use-event-stream.js.map +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +206 -4
- package/dist/tempest-react-sdk.js +122 -120
- package/package.json +1 -1
|
@@ -84,6 +84,8 @@ function spec(paths, schemas = {}) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const OK = { 200: { description: "ok" } };
|
|
87
|
+
const schemaRef = (name) => ({ $ref: `#/components/schemas/${name}` });
|
|
88
|
+
const json = (schema) => ({ content: { "application/json": { schema } } });
|
|
87
89
|
|
|
88
90
|
describe("generate — hostile spec text never becomes code (#434)", () => {
|
|
89
91
|
it("a tag that closes the class doc comment stays inside it", async () => {
|
|
@@ -233,12 +235,45 @@ describe("generate — hostile spec text never becomes code (#434)", () => {
|
|
|
233
235
|
await runGroup(files, "t");
|
|
234
236
|
});
|
|
235
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Rewritten for #516: this used to pass with `spec({}, …)`, two schemas no
|
|
240
|
+
* route reaches. The check now runs over the schemas a group emits, so the
|
|
241
|
+
* pair has to be referenced to collide — and still is refused, naming both.
|
|
242
|
+
*/
|
|
236
243
|
it("two schema names that map to the same identifier are refused, naming both", () => {
|
|
244
|
+
const both = {
|
|
245
|
+
200: json({ anyOf: [schemaRef("User-Out"), schemaRef("user.Out")] }),
|
|
246
|
+
};
|
|
237
247
|
expect(() =>
|
|
238
|
-
generate(
|
|
248
|
+
generate(
|
|
249
|
+
spec(
|
|
250
|
+
{ "/a": { get: { tags: ["t"], responses: both } } },
|
|
251
|
+
{ "User-Out": { type: "object" }, "user.Out": { type: "object" } },
|
|
252
|
+
),
|
|
253
|
+
),
|
|
239
254
|
).toThrow(/"User-Out" and "user\.Out" both map to the TypeScript identifier "UserOut"/);
|
|
240
255
|
});
|
|
241
256
|
|
|
257
|
+
it("schema names no route reaches are neither emitted nor checked, so an orphan cannot abort the run (#516)", () => {
|
|
258
|
+
const { files } = generate(
|
|
259
|
+
spec(
|
|
260
|
+
{
|
|
261
|
+
"/a": {
|
|
262
|
+
get: { tags: ["t"], responses: { 200: json(schemaRef("MediaType")) } },
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
MediaType: { type: "string", enum: ["image/png"] },
|
|
267
|
+
"Media-Type": { type: "string", enum: ["image/png", "image/webp"] },
|
|
268
|
+
"***": { type: "object" },
|
|
269
|
+
},
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
expect(files["t/types.ts"]).toContain("export type MediaType = ");
|
|
273
|
+
expect(files["t/schemas.ts"]).not.toContain("image/webp");
|
|
274
|
+
expect(files["t/types.ts"].match(/export type/g)).toHaveLength(1);
|
|
275
|
+
});
|
|
276
|
+
|
|
242
277
|
it("a path parameter that maps onto another method argument is refused", () => {
|
|
243
278
|
expect(() =>
|
|
244
279
|
generate(
|
|
@@ -258,10 +293,16 @@ describe("generate — hostile spec text never becomes code (#434)", () => {
|
|
|
258
293
|
).toThrow(/"user-id" and "userId" both map to the TypeScript identifier "userId"/);
|
|
259
294
|
});
|
|
260
295
|
|
|
296
|
+
/** Rewritten for #516 like the collision above: the schema is now referenced. */
|
|
261
297
|
it("a name with nothing to build an identifier from is refused", () => {
|
|
262
|
-
expect(() =>
|
|
263
|
-
|
|
264
|
-
|
|
298
|
+
expect(() =>
|
|
299
|
+
generate(
|
|
300
|
+
spec(
|
|
301
|
+
{ "/a": { get: { tags: ["t"], responses: { 200: json(schemaRef("***")) } } } },
|
|
302
|
+
{ "***": { type: "object" } },
|
|
303
|
+
),
|
|
304
|
+
),
|
|
305
|
+
).toThrow(/schema name "\*\*\*" has no letter or digit/);
|
|
265
306
|
});
|
|
266
307
|
|
|
267
308
|
it("a regex pattern with an escaped slash or a line break compiles to the same regex", () => {
|
|
@@ -50,12 +50,88 @@ function methodName(op, method, path) {
|
|
|
50
50
|
const CLASS_MEMBERS = ["constructor", "api"];
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* Names a generated method body uses besides its path parameters: the `body
|
|
54
|
-
* `params` arguments, the `S` schemas import
|
|
55
|
-
*
|
|
56
|
-
* `bodyParam`), so it neither
|
|
53
|
+
* Names a generated method body uses besides its path parameters: the `body`,
|
|
54
|
+
* `params` and `headers` arguments, the `S` schemas import, the `encodeIdSegment`
|
|
55
|
+
* import and the module-level `headerRecord` helper. A path parameter that maps to
|
|
56
|
+
* one of them becomes `<name>Param` (`{body}` → `bodyParam`), so it neither
|
|
57
|
+
* duplicates an argument nor shadows the import.
|
|
57
58
|
*/
|
|
58
|
-
const METHOD_LOCALS = new Set([
|
|
59
|
+
const METHOD_LOCALS = new Set([
|
|
60
|
+
"body",
|
|
61
|
+
"params",
|
|
62
|
+
"headers",
|
|
63
|
+
"S",
|
|
64
|
+
"encodeIdSegment",
|
|
65
|
+
"headerRecord",
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Header parameter names OpenAPI 3 says a generator SHALL ignore: the client sets
|
|
70
|
+
* `Accept` and `Content-Type` from the request it builds and `Authorization` from
|
|
71
|
+
* its `getToken`, so a spec that also declares them must not open a second way to
|
|
72
|
+
* send them. Compared case-insensitively, as HTTP header names are.
|
|
73
|
+
*/
|
|
74
|
+
const IGNORED_HEADERS = new Set(["accept", "content-type", "authorization"]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Module-level helper a `service.ts` declares when one of its methods sends header
|
|
78
|
+
* parameters. `fetch` turns a header value of `undefined` into the literal text
|
|
79
|
+
* `"undefined"`, and `RequestOptions.headers` only takes strings, so an optional
|
|
80
|
+
* header left out is dropped and every other value goes through `String()`.
|
|
81
|
+
*/
|
|
82
|
+
const HEADER_RECORD_HELPER = [
|
|
83
|
+
"/** Header parameters as strings, without the ones left out (`fetch` would send `undefined` as text). */",
|
|
84
|
+
"function headerRecord(",
|
|
85
|
+
" values: Record<string, string | number | boolean | null | undefined>,",
|
|
86
|
+
"): Record<string, string> {",
|
|
87
|
+
" const out: Record<string, string> = {};",
|
|
88
|
+
" for (const [name, value] of Object.entries(values)) {",
|
|
89
|
+
" if (value !== undefined && value !== null) out[name] = String(value);",
|
|
90
|
+
" }",
|
|
91
|
+
" return out;",
|
|
92
|
+
"}",
|
|
93
|
+
].join("\n");
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Follow a `#/components/parameters/...` `$ref` chain to the parameter object.
|
|
97
|
+
*
|
|
98
|
+
* @param {object | undefined} param - A parameter object or a reference to one.
|
|
99
|
+
* @param {Record<string, object>} components - `components.parameters`.
|
|
100
|
+
* @returns {object | undefined} The parameter object, or `undefined` when the
|
|
101
|
+
* chain does not resolve.
|
|
102
|
+
*/
|
|
103
|
+
function resolveParameter(param, components) {
|
|
104
|
+
let node = param;
|
|
105
|
+
let guard = 0;
|
|
106
|
+
while (node && node.$ref && guard++ < 16) node = components[refName(node.$ref)];
|
|
107
|
+
return node && !node.$ref ? node : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The parameters that apply to one operation: the path item's `parameters`
|
|
112
|
+
* merged with the operation's, the operation winning on the same `(name, in)` —
|
|
113
|
+
* the rule OpenAPI 3 sets for the two levels.
|
|
114
|
+
*
|
|
115
|
+
* Before #509 only `operation.parameters` was read, so a `{id}` declared on the
|
|
116
|
+
* path item reached the URL literally (`"/items/{id}"`, measured), and a path item
|
|
117
|
+
* declaring one placeholder while the operation declared the other refused to
|
|
118
|
+
* generate. `$ref`s into `components.parameters` are resolved here too: they were
|
|
119
|
+
* dropped the same way, because a reference object has no `in`.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} item - The path item.
|
|
122
|
+
* @param {object} op - The operation.
|
|
123
|
+
* @param {Record<string, object>} components - `components.parameters`.
|
|
124
|
+
* @returns {object[]} The effective parameter objects, path-item order first.
|
|
125
|
+
*/
|
|
126
|
+
function operationParameters(item, op, components) {
|
|
127
|
+
const merged = new Map();
|
|
128
|
+
for (const raw of [...(item.parameters ?? []), ...(op.parameters ?? [])]) {
|
|
129
|
+
const param = resolveParameter(raw, components);
|
|
130
|
+
if (!param || typeof param.name !== "string" || typeof param.in !== "string") continue;
|
|
131
|
+
merged.set(`${param.in}\u0000${param.name}`, param);
|
|
132
|
+
}
|
|
133
|
+
return [...merged.values()];
|
|
134
|
+
}
|
|
59
135
|
|
|
60
136
|
/**
|
|
61
137
|
* Whether a path parameter's schema only ever produces a `string` or a `number` —
|
|
@@ -208,16 +284,17 @@ function isQueryScalar(schema, schemas, depth = 0) {
|
|
|
208
284
|
}
|
|
209
285
|
|
|
210
286
|
/**
|
|
211
|
-
* TS type for a query-param value.
|
|
212
|
-
* client
|
|
213
|
-
* (objects, arrays) is
|
|
287
|
+
* TS type for a query- or header-param value. Both serialize to strings, so the
|
|
288
|
+
* client's `params` and the generated `headerRecord` only accept
|
|
289
|
+
* `string | number | boolean | null`. Anything richer (objects, arrays) is
|
|
290
|
+
* narrowed to `string` to stay assignable.
|
|
214
291
|
*
|
|
215
292
|
* @param {object | undefined} schema - The parameter's schema.
|
|
216
293
|
* @param {Record<string, object>} schemas - `components.schemas`.
|
|
217
294
|
* @param {Set<string>} refs - Collects the schemas the type names.
|
|
218
295
|
* @returns {string} The TypeScript type expression.
|
|
219
296
|
*/
|
|
220
|
-
function
|
|
297
|
+
function scalarParamType(schema, schemas, refs) {
|
|
221
298
|
const node = schema ?? { type: "string" };
|
|
222
299
|
return isQueryScalar(node, schemas) ? tsType(node, refs) : "string";
|
|
223
300
|
}
|
|
@@ -343,16 +420,25 @@ function compareNames(a, b) {
|
|
|
343
420
|
* template's ESLint enforces, so the output passes its lint and `tsc` with
|
|
344
421
|
* `noUnusedLocals` as generated (#504, #505).
|
|
345
422
|
*
|
|
423
|
+
* A component schema no route reaches is never emitted — FastAPI publishes an
|
|
424
|
+
* unreferenced enum for every `Enum`-annotated field (`ImageMediaType`, #516),
|
|
425
|
+
* and it lands in no group. Its name is not checked either: the identifier
|
|
426
|
+
* collision check runs per group over the schemas that group emits, so an orphan
|
|
427
|
+
* named `Media-Type` next to a referenced `MediaType` no longer aborts the whole
|
|
428
|
+
* run for a type nobody would see.
|
|
429
|
+
*
|
|
430
|
+
* Parameters are read from the path item and the operation, merged by
|
|
431
|
+
* `(name, in)` (see {@link operationParameters}); `in: "cookie"` is skipped,
|
|
432
|
+
* because a browser `fetch` cannot set `Cookie` — the browser sends its own
|
|
433
|
+
* cookies with `credentials`, which is how a cookie parameter reaches the server.
|
|
434
|
+
*
|
|
346
435
|
* @param {object} doc - The parsed OpenAPI 3.x spec.
|
|
347
436
|
* @returns {{ files: Record<string, string>, tags: string[] }}
|
|
348
437
|
*/
|
|
349
438
|
export function generate(doc) {
|
|
350
439
|
const schemas = doc.components?.schemas ?? {};
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
"schema names",
|
|
354
|
-
);
|
|
355
|
-
const groups = new Map(); // slug → { tag, ops: [...] }
|
|
440
|
+
const parameterComponents = doc.components?.parameters ?? {};
|
|
441
|
+
const groups = new Map();
|
|
356
442
|
|
|
357
443
|
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
358
444
|
for (const method of HTTP_METHODS) {
|
|
@@ -361,7 +447,8 @@ export function generate(doc) {
|
|
|
361
447
|
const tag = op.tags?.[0] ?? "default";
|
|
362
448
|
const slug = tagSlug(tag);
|
|
363
449
|
if (!groups.has(slug)) groups.set(slug, { tag, slug, ops: [] });
|
|
364
|
-
|
|
450
|
+
const params = operationParameters(item, op, parameterComponents);
|
|
451
|
+
groups.get(slug).ops.push({ method, path, op, params });
|
|
365
452
|
}
|
|
366
453
|
}
|
|
367
454
|
|
|
@@ -378,9 +465,9 @@ export function generate(doc) {
|
|
|
378
465
|
const Class = `${slugIdent(pascal(slug))}Service`;
|
|
379
466
|
|
|
380
467
|
const usedNames = new Set(CLASS_MEMBERS);
|
|
381
|
-
const signature = { refs: new Set(), validates: false };
|
|
382
|
-
const methods = ops.map(({ method, path, op }) =>
|
|
383
|
-
emitMethod(method, path, op, schemas, usedNames, signature),
|
|
468
|
+
const signature = { refs: new Set(), validates: false, sendsHeaders: false };
|
|
469
|
+
const methods = ops.map(({ method, path, op, params }) =>
|
|
470
|
+
emitMethod(method, path, op, params, schemas, usedNames, signature),
|
|
384
471
|
);
|
|
385
472
|
|
|
386
473
|
const used = new Set();
|
|
@@ -392,6 +479,10 @@ export function generate(doc) {
|
|
|
392
479
|
}
|
|
393
480
|
for (const name of signature.refs) collectRefs({ $ref: name }, schemas, used);
|
|
394
481
|
const { sorted, cyclic } = topoSort(used, schemas);
|
|
482
|
+
assertNoCollision(
|
|
483
|
+
sorted.map((n) => [n, typeName(n)]),
|
|
484
|
+
"schema names",
|
|
485
|
+
);
|
|
395
486
|
for (const name of sorted) assertPatterns(schemas[name], name);
|
|
396
487
|
|
|
397
488
|
const schemaLines = sorted.map((name) => {
|
|
@@ -433,6 +524,7 @@ export function generate(doc) {
|
|
|
433
524
|
.filter((group) => group.length)
|
|
434
525
|
.map((group) => group.join("\n"))
|
|
435
526
|
.join("\n\n") +
|
|
527
|
+
(signature.sendsHeaders ? `\n\n${HEADER_RECORD_HELPER}` : "") +
|
|
436
528
|
`\n\n/** Generated service for the "${commentText(tag)}" routes. Inject an ApiClient (createApiClient). */\n` +
|
|
437
529
|
`export class ${Class} {\n` +
|
|
438
530
|
` constructor(private readonly api: ApiClient) {}\n\n` +
|
|
@@ -461,16 +553,27 @@ export function generate(doc) {
|
|
|
461
553
|
* pagination envelope maps to `OffsetPage<T>` / `CursorPage<T>`, and a body typed
|
|
462
554
|
* by a `$ref` is validated with its Zod schema before the call.
|
|
463
555
|
*
|
|
556
|
+
* Arguments come in a fixed order: path parameters, `body`, `params` (query),
|
|
557
|
+
* `headers`. Header parameters are typed like query parameters and sent through
|
|
558
|
+
* the request's `headers`, via `headerRecord`; the `headers` argument defaults to
|
|
559
|
+
* `{}` when none of them is required, so a spec gaining an optional header does
|
|
560
|
+
* not break the call sites. `Accept`, `Content-Type` and `Authorization` are left
|
|
561
|
+
* out ({@link IGNORED_HEADERS}), and so is every `in: "cookie"` parameter: a
|
|
562
|
+
* browser `fetch` cannot set `Cookie` (forbidden header name), the cookie jar
|
|
563
|
+
* sends it with `credentials`.
|
|
564
|
+
*
|
|
464
565
|
* @param {string} method - The HTTP method, lower case.
|
|
465
566
|
* @param {string} path - The OpenAPI path template.
|
|
466
567
|
* @param {object} op - The operation object.
|
|
568
|
+
* @param {object[]} parameters - The effective parameters, from {@link operationParameters}.
|
|
467
569
|
* @param {Record<string, object>} schemas - `components.schemas`.
|
|
468
570
|
* @param {Set<string>} usedNames - Member names already taken in the class.
|
|
469
|
-
* @param {{ refs: Set<string>, validates: boolean }} signature -
|
|
470
|
-
* component schemas the emitted types name,
|
|
571
|
+
* @param {{ refs: Set<string>, validates: boolean, sendsHeaders: boolean }} signature -
|
|
572
|
+
* Collects the component schemas the emitted types name, whether `S` was used
|
|
573
|
+
* and whether `headerRecord` was.
|
|
471
574
|
* @returns {string} The method source.
|
|
472
575
|
*/
|
|
473
|
-
function emitMethod(method, path, op, schemas, usedNames, signature) {
|
|
576
|
+
function emitMethod(method, path, op, parameters, schemas, usedNames, signature) {
|
|
474
577
|
let name = methodName(op, method, path);
|
|
475
578
|
if (usedNames.has(name)) {
|
|
476
579
|
let n = 2;
|
|
@@ -478,7 +581,11 @@ function emitMethod(method, path, op, schemas, usedNames, signature) {
|
|
|
478
581
|
name = `${name}${n}`;
|
|
479
582
|
}
|
|
480
583
|
usedNames.add(name);
|
|
481
|
-
const pathParams =
|
|
584
|
+
const pathParams = parameters.filter((p) => p.in === "path");
|
|
585
|
+
const queryParams = parameters.filter((p) => p.in === "query");
|
|
586
|
+
const headerParams = parameters.filter(
|
|
587
|
+
(p) => p.in === "header" && !IGNORED_HEADERS.has(p.name.toLowerCase()),
|
|
588
|
+
);
|
|
482
589
|
const where = `${method.toUpperCase()} ${path}`;
|
|
483
590
|
const paramId = (name) => {
|
|
484
591
|
const id = toIdentifier(name, "camel", `path parameter in ${where}`);
|
|
@@ -496,13 +603,11 @@ function emitMethod(method, path, op, schemas, usedNames, signature) {
|
|
|
496
603
|
[
|
|
497
604
|
...[...paramIds],
|
|
498
605
|
...(bodySchema(op) ? [["the request body argument", "body"]] : []),
|
|
499
|
-
...(
|
|
500
|
-
|
|
501
|
-
: []),
|
|
606
|
+
...(queryParams.length ? [["the query argument", "params"]] : []),
|
|
607
|
+
...(headerParams.length ? [["the headers argument", "headers"]] : []),
|
|
502
608
|
],
|
|
503
609
|
`method arguments of ${where}`,
|
|
504
610
|
);
|
|
505
|
-
const queryParams = (op.parameters ?? []).filter((p) => p.in === "query");
|
|
506
611
|
const body = bodySchema(op);
|
|
507
612
|
const resp = successSchema(op);
|
|
508
613
|
const page = resp ? detectPage(resp, schemas, signature.refs) : null;
|
|
@@ -512,6 +617,14 @@ function emitMethod(method, path, op, schemas, usedNames, signature) {
|
|
|
512
617
|
? tsType(resp, signature.refs)
|
|
513
618
|
: "void";
|
|
514
619
|
|
|
620
|
+
const fields = (list) =>
|
|
621
|
+
list
|
|
622
|
+
.map(
|
|
623
|
+
(p) =>
|
|
624
|
+
`${JSON.stringify(p.name)}${p.required ? "" : "?"}: ${scalarParamType(p.schema, schemas, signature.refs)}`,
|
|
625
|
+
)
|
|
626
|
+
.join("; ");
|
|
627
|
+
|
|
515
628
|
const args = [];
|
|
516
629
|
for (const p of pathParams) {
|
|
517
630
|
args.push(
|
|
@@ -519,14 +632,11 @@ function emitMethod(method, path, op, schemas, usedNames, signature) {
|
|
|
519
632
|
);
|
|
520
633
|
}
|
|
521
634
|
if (body) args.push(`body: ${tsType(body, signature.refs)}`);
|
|
522
|
-
if (queryParams.length) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
)
|
|
528
|
-
.join("; ");
|
|
529
|
-
args.push(`params: { ${q} }`);
|
|
635
|
+
if (queryParams.length) args.push(`params: { ${fields(queryParams)} }`);
|
|
636
|
+
if (headerParams.length) {
|
|
637
|
+
const optional = headerParams.every((p) => !p.required);
|
|
638
|
+
args.push(`headers: { ${fields(headerParams)} }${optional ? " = {}" : ""}`);
|
|
639
|
+
signature.sendsHeaders = true;
|
|
530
640
|
}
|
|
531
641
|
|
|
532
642
|
const url = urlTemplate(path, segmentArgs);
|
|
@@ -534,6 +644,7 @@ function emitMethod(method, path, op, schemas, usedNames, signature) {
|
|
|
534
644
|
const callOpts = [];
|
|
535
645
|
if (body) callOpts.push("body");
|
|
536
646
|
if (queryParams.length) callOpts.push("params");
|
|
647
|
+
if (headerParams.length) callOpts.push("headers: headerRecord(headers)");
|
|
537
648
|
const optsArg = callOpts.length ? `, { ${callOpts.join(", ")} }` : "";
|
|
538
649
|
|
|
539
650
|
let validation = "";
|
|
@@ -6,6 +6,7 @@ import { ESLint } from "eslint";
|
|
|
6
6
|
import ts from "typescript";
|
|
7
7
|
import { describe, expect, it } from "vitest";
|
|
8
8
|
|
|
9
|
+
import { encodeIdSegment } from "../../../src/data";
|
|
9
10
|
import { generate } from "./generate.mjs";
|
|
10
11
|
|
|
11
12
|
const SPEC = {
|
|
@@ -175,7 +176,7 @@ function templateTsc(files) {
|
|
|
175
176
|
virtual.set(
|
|
176
177
|
join(OUT, "node_modules", "tempest-react-sdk", "index.d.ts"),
|
|
177
178
|
[
|
|
178
|
-
"type Call = <T = unknown>(url: string, opts?: { body?: unknown; params?: Record<string, string | number | boolean | undefined | null> }) => Promise<T>;",
|
|
179
|
+
"type Call = <T = unknown>(url: string, opts?: { body?: unknown; params?: Record<string, string | number | boolean | undefined | null>; headers?: Record<string, string> }) => Promise<T>;",
|
|
179
180
|
"export type ApiClient = { get: Call; post: Call; put: Call; patch: Call; delete: Call };",
|
|
180
181
|
"export type OffsetPage<T> = { items: T[]; total: number };",
|
|
181
182
|
"export type CursorPage<T> = { items: T[]; next_cursor: string | null };",
|
|
@@ -244,8 +245,8 @@ const json = (schema) => ({ content: { "application/json": { schema } } });
|
|
|
244
245
|
* | every `types.ts` | `import * as S` used only in `typeof` — `@typescript-eslint/consistent-type-imports` |
|
|
245
246
|
* | every `index.ts` + root barrel | `simple-import-sort/exports` |
|
|
246
247
|
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
248
|
+
* The `X-Trace` header parameter was not read at all then; since #509 it is, and
|
|
249
|
+
* its `Code` type is imported like any other signature type.
|
|
249
250
|
*/
|
|
250
251
|
const EVERY_POSITION = {
|
|
251
252
|
openapi: "3.1.0",
|
|
@@ -361,7 +362,7 @@ describe("generate — output passes the create-tempest-app template's tsc and E
|
|
|
361
362
|
|
|
362
363
|
it("service.ts imports only the types its signatures name, and S only when a body is validated", () => {
|
|
363
364
|
expect(files["items/service.ts"]).toContain(
|
|
364
|
-
'import type { Item, ItemCreate, Kind, Sort } from "./types";',
|
|
365
|
+
'import type { Code, Item, ItemCreate, Kind, Sort } from "./types";',
|
|
365
366
|
);
|
|
366
367
|
expect(files["items/service.ts"]).not.toContain('import * as S from "./schemas";');
|
|
367
368
|
expect(files["reports/service.ts"]).not.toContain("./schemas");
|
|
@@ -394,3 +395,201 @@ describe("generate — output passes the create-tempest-app template's tsc and E
|
|
|
394
395
|
expect(await templateLint(out)).toEqual([]);
|
|
395
396
|
});
|
|
396
397
|
});
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Transpile a generated `service.ts` and instantiate its class with a recording
|
|
401
|
+
* `ApiClient`, to read back the URL and options each call actually sends.
|
|
402
|
+
* `encodeIdSegment` is the SDK's real one.
|
|
403
|
+
*
|
|
404
|
+
* @param {string} source - The generated `service.ts`.
|
|
405
|
+
* @returns {{ service: object, calls: Array<[string, object | undefined]> }}
|
|
406
|
+
*/
|
|
407
|
+
function instantiate(source) {
|
|
408
|
+
const { outputText } = ts.transpileModule(source, {
|
|
409
|
+
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
|
410
|
+
});
|
|
411
|
+
const module = { exports: {} };
|
|
412
|
+
const requireStub = (id) => {
|
|
413
|
+
if (id === "tempest-react-sdk") return { encodeIdSegment };
|
|
414
|
+
throw new Error(`unexpected import ${id}`);
|
|
415
|
+
};
|
|
416
|
+
new Function("exports", "require", "module", outputText)(module.exports, requireStub, module);
|
|
417
|
+
const calls = [];
|
|
418
|
+
const api = new Proxy(
|
|
419
|
+
{},
|
|
420
|
+
{
|
|
421
|
+
get: () => async (url, opts) => {
|
|
422
|
+
calls.push([url, opts]);
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
);
|
|
426
|
+
const Class = Object.values(module.exports).find((v) => typeof v === "function");
|
|
427
|
+
return { service: new Class(api), calls };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Measured on `main` before #509 with minimal specs run through `generate`:
|
|
432
|
+
*
|
|
433
|
+
* | Spec | Output before the fix |
|
|
434
|
+
* | --- | --- |
|
|
435
|
+
* | `{id}` declared only on the path item | `getItem(): Promise<void>` calling `"/items/{id}"` — placeholder sent literally |
|
|
436
|
+
* | path item declares `{x}`, operation declares `{y}` | refused: `placeholder "x" is not a declared path parameter` |
|
|
437
|
+
* | required `in: "header"` | `list()` — no argument, nothing sent |
|
|
438
|
+
* | `in: "cookie"` | ignored (stays ignored, on purpose: `fetch` cannot set `Cookie`) |
|
|
439
|
+
* | path-item query + operation override | the path item's `q` lost; the override only looked right because the path item was never read |
|
|
440
|
+
* | parameter `$ref` into `components.parameters` | dropped like the path-item case: `"/items/{id}"` literal |
|
|
441
|
+
*/
|
|
442
|
+
const PARAMS = {
|
|
443
|
+
openapi: "3.1.0",
|
|
444
|
+
components: {
|
|
445
|
+
schemas: { Region: { type: "string", enum: ["br", "us"] } },
|
|
446
|
+
parameters: {
|
|
447
|
+
Version: { name: "v", in: "path", required: true, schema: { type: "integer" } },
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
paths: {
|
|
451
|
+
"/items/{id}": {
|
|
452
|
+
parameters: [
|
|
453
|
+
{ name: "id", in: "path", required: true, schema: { type: "string" } },
|
|
454
|
+
{ name: "X-Tenant-Id", in: "header", required: true, schema: { type: "string" } },
|
|
455
|
+
{ name: "limit", in: "query", schema: { type: "string" } },
|
|
456
|
+
],
|
|
457
|
+
get: {
|
|
458
|
+
tags: ["items"],
|
|
459
|
+
operationId: "get_item",
|
|
460
|
+
parameters: [
|
|
461
|
+
{ name: "limit", in: "query", required: true, schema: { type: "integer" } },
|
|
462
|
+
{ name: "X-Region", in: "header", schema: ref("Region") },
|
|
463
|
+
{ name: "Retry-Count", in: "header", schema: { type: "integer" } },
|
|
464
|
+
{ name: "session", in: "cookie", schema: { type: "string" } },
|
|
465
|
+
{ name: "Authorization", in: "header", schema: { type: "string" } },
|
|
466
|
+
],
|
|
467
|
+
responses: { 200: { description: "ok" } },
|
|
468
|
+
},
|
|
469
|
+
delete: {
|
|
470
|
+
tags: ["items"],
|
|
471
|
+
operationId: "delete_item",
|
|
472
|
+
parameters: [
|
|
473
|
+
{
|
|
474
|
+
name: "X-Tenant-Id",
|
|
475
|
+
in: "header",
|
|
476
|
+
required: false,
|
|
477
|
+
schema: { type: "string" },
|
|
478
|
+
},
|
|
479
|
+
],
|
|
480
|
+
responses: { 204: { description: "gone" } },
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
"/v{v}/items": {
|
|
484
|
+
post: {
|
|
485
|
+
tags: ["items"],
|
|
486
|
+
operationId: "create_item",
|
|
487
|
+
parameters: [
|
|
488
|
+
{ $ref: "#/components/parameters/Version" },
|
|
489
|
+
{ name: "Idempotency-Key", in: "header", schema: { type: "string" } },
|
|
490
|
+
],
|
|
491
|
+
responses: { 201: { description: "ok" } },
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
describe("generate — path-item, header and $ref parameters (#509)", () => {
|
|
498
|
+
const { files } = generate(PARAMS);
|
|
499
|
+
const svc = files["items/service.ts"];
|
|
500
|
+
|
|
501
|
+
it("a path parameter declared only on the path item is an argument and goes through encodeIdSegment", () => {
|
|
502
|
+
expect(svc).not.toContain('"/items/{id}"');
|
|
503
|
+
expect(svc).toContain("`/items/${encodeIdSegment(id)}`");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("path-item and operation parameters merge, the operation winning on the same (name, in)", () => {
|
|
507
|
+
expect(svc).toContain(
|
|
508
|
+
'async getItem(id: string, params: { "limit": number }, headers: { "X-Tenant-Id": string; "X-Region"?: Region; "Retry-Count"?: number }): Promise<void>',
|
|
509
|
+
);
|
|
510
|
+
expect(svc).toContain(
|
|
511
|
+
'async deleteItem(id: string, params: { "limit"?: string }, headers: { "X-Tenant-Id"?: string } = {}): Promise<void>',
|
|
512
|
+
);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("a parameter $ref into components.parameters is resolved", () => {
|
|
516
|
+
expect(svc).toContain("async createItem(v: number, headers: ");
|
|
517
|
+
expect(svc).toContain("`/v${encodeIdSegment(v)}/items`");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("cookie parameters and the Accept/Content-Type/Authorization headers are not emitted", () => {
|
|
521
|
+
expect(svc).not.toContain("session");
|
|
522
|
+
expect(svc).not.toContain("Authorization");
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("header values are sent as strings, and an optional header left out is not sent as 'undefined'", async () => {
|
|
526
|
+
const { service, calls } = instantiate(svc);
|
|
527
|
+
await service.getItem("a/b", { limit: 5 }, { "X-Tenant-Id": "t1", "Retry-Count": 3 });
|
|
528
|
+
await service.deleteItem("7", {});
|
|
529
|
+
await service.createItem(2);
|
|
530
|
+
expect(calls).toEqual([
|
|
531
|
+
[
|
|
532
|
+
"/items/a%2Fb",
|
|
533
|
+
{ params: { limit: 5 }, headers: { "X-Tenant-Id": "t1", "Retry-Count": "3" } },
|
|
534
|
+
],
|
|
535
|
+
["/items/7", { params: {}, headers: {} }],
|
|
536
|
+
["/v2/items", { headers: {} }],
|
|
537
|
+
]);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("type-checks and lints clean under the create-tempest-app template", async () => {
|
|
541
|
+
expect(templateTsc(files)).toEqual([]);
|
|
542
|
+
expect(await templateLint(files)).toEqual([]);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* FastAPI publishes a component for every `Enum`-annotated field even when the
|
|
548
|
+
* field's own schema carries a narrower inline enum, so `ImageMediaType` appears
|
|
549
|
+
* with no `$ref` pointing at it. Measured on `main` before #516: it was already
|
|
550
|
+
* left out of every group, because a group only emits the schemas its routes
|
|
551
|
+
* reach. What did break was the name check, which ran over every
|
|
552
|
+
* `components.schemas` entry — an orphan whose name collided with another
|
|
553
|
+
* (`ImageMediaType` / `Image-Media-Type`) aborted the whole run.
|
|
554
|
+
*/
|
|
555
|
+
const ORPHAN_ENUM = {
|
|
556
|
+
openapi: "3.1.0",
|
|
557
|
+
components: {
|
|
558
|
+
schemas: {
|
|
559
|
+
ImageMediaType: { type: "string", enum: ["image/png", "image/jpeg", "image/webp"] },
|
|
560
|
+
"Image-Media-Type": { type: "string", enum: ["image/gif"] },
|
|
561
|
+
Kind: { type: "string", enum: ["photo", "avatar"] },
|
|
562
|
+
Upload: {
|
|
563
|
+
type: "object",
|
|
564
|
+
properties: {
|
|
565
|
+
kind: ref("Kind"),
|
|
566
|
+
media_type: { type: "string", enum: ["image/png", "image/jpeg"] },
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
paths: {
|
|
572
|
+
"/uploads": {
|
|
573
|
+
post: {
|
|
574
|
+
tags: ["uploads"],
|
|
575
|
+
operationId: "create_upload",
|
|
576
|
+
requestBody: json(ref("Upload")),
|
|
577
|
+
responses: { 201: json(ref("Upload")) },
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
describe("generate — enum schemas no $ref reaches (#516)", () => {
|
|
584
|
+
it("keeps an enum reached transitively and drops the orphan, even when the orphan's name collides", async () => {
|
|
585
|
+
const { files } = generate(ORPHAN_ENUM);
|
|
586
|
+
expect(files["uploads/types.ts"]).toContain("export type Kind = ");
|
|
587
|
+
expect(files["uploads/schemas.ts"]).toContain(
|
|
588
|
+
'export const KindSchema = z.enum(["photo", "avatar"]);',
|
|
589
|
+
);
|
|
590
|
+
expect(Object.values(files).join("\n")).not.toContain("ImageMediaType");
|
|
591
|
+
expect(Object.values(files).join("\n")).not.toContain("image/webp");
|
|
592
|
+
expect(templateTsc(files)).toEqual([]);
|
|
593
|
+
expect(await templateLint(files)).toEqual([]);
|
|
594
|
+
});
|
|
595
|
+
});
|
|
@@ -15,9 +15,9 @@ export function refName(ref) {
|
|
|
15
15
|
/**
|
|
16
16
|
* Names the generated files declare, import or rely on as globals next to the
|
|
17
17
|
* schema types: the SDK types and the `encodeIdSegment` value `service.ts`
|
|
18
|
-
* imports, the `
|
|
19
|
-
* emitted signatures use. A schema with one
|
|
20
|
-
* the import or shadow the global.
|
|
18
|
+
* imports, the `headerRecord` helper it declares, the `z` and `S` imports, and
|
|
19
|
+
* the `Promise` / `Record` globals the emitted signatures use. A schema with one
|
|
20
|
+
* of these names would collide with the import or shadow the global.
|
|
21
21
|
*/
|
|
22
22
|
export const GENERATED_NAMES = new Set([
|
|
23
23
|
"ApiClient",
|
|
@@ -27,6 +27,7 @@ export const GENERATED_NAMES = new Set([
|
|
|
27
27
|
"Record",
|
|
28
28
|
"S",
|
|
29
29
|
"encodeIdSegment",
|
|
30
|
+
"headerRecord",
|
|
30
31
|
"z",
|
|
31
32
|
]);
|
|
32
33
|
|
package/dist/http/api-client.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./credential-scope.cjs"),r=require("./csrf.cjs"),i=require("./decode-response.cjs"),a=require("./errors.cjs"),o=require("./retry.cjs"),s=require("./timeout.cjs");var c=new Set([`GET`,`HEAD`,`OPTIONS`]);function l(e,t){return!c.has(t)||!(e instanceof a.TempestApiError)?!1:a.isRetriableStatus(e.status)}function u(e){return e?e===!0?{}:e:null}var d=15e3,f=3e5;function p(e){return typeof FormData<`u`&&e instanceof FormData}function m(e){return Math.round(performance.now()-e)}async function h(e,t){let n;try{n=await e.clone().json()}catch{try{n=await e.text()}catch{n=null}}return new a.TempestApiError(a.buildApiError(e.status,n,e.headers,t))}function g(c){let g=c.fetcher??globalThis.fetch.bind(globalThis);function _(e,t){if(t)return{};let r=c.getToken?.();return r?n.isTrustedCredentialTarget(e,c.baseURL,c.trustedOrigins)?{Authorization:`Bearer ${r}`}:(n.reportSuppressedCredential(e,c.baseURL),{}):{}}async function v(e,n,i,o){let{body:l,params:u,headers:m,signal:h,timeout:v,...y}=n,b=p(l),x=b?c.uploadTimeout:c.timeout,S=v===void 0?x===void 0?b?f:d:x:v,C=t.buildApiUrl(c.baseURL,e,{prefix:c.prefix,params:u}),w=m,T={...b?{}:{"Content-Type":`application/json`},...i?{"X-Request-ID":i}:{},...c.headers,..._(C,o),...r.csrfHeaders({method:y.method??`GET`,url:C,reference:c.baseURL,csrf:c.csrf,trustedOrigins:c.trustedOrigins,headers:{...c.headers,...w}}),...w},E=s.withTimeout(h,S),D={...y,signal:E.signal,headers:T,credentials:c.withCredentials?`include`:y.credentials,body:l==null?void 0:b?l:JSON.stringify(l)};try{return await g(C,D)}catch(e){throw E.timedOut()?new a.TempestApiError({status:0,detail:`A requisição excedeu ${S}ms e foi abandonada.`}):e}finally{E.dispose()}}async function y(e,t,n,r,i){let a=c.logger;if(!a)return v(e,t,n,i);let o=performance.now();try{let s=await v(e,t,n,i),c={requestId:n,status:s.status,ms:m(o)},l=`${r} ${e} → ${s.status}`;return s.status>=400?a.warn(l,c):a.debug(l,c),s}catch(t){throw a.warn(`${r} ${e} → no response`,{requestId:n,ms:m(o),error:t}),t}}async function b(e,t,n){if(c.onUnauthorized){c.logger?.warn(`unauthorized — calling onUnauthorized`,{requestId:t,status:e.status});try{await c.onUnauthorized(e,n)}catch(n){if(n instanceof a.SessionSupersededError)throw n;c.logger?.warn(`onUnauthorized threw — keeping the original response error`,{requestId:t,status:e.status,error:n})}}}function x(e){return{token:e?null:c.getToken?.()??null,epoch:c.getSessionEpoch?.()??null}}async function S(t,n,r){let{skipAuth:i=!1,skipAuthRetry:o=!1,...s}=n,l=c.requestId?c.requestId():e.randomId(),u=(s.method??`GET`).toUpperCase(),d=x(i),f=await y(t,s,l,u,i);if(f.status===401&&!i&&!o){if(c.refresh){let e;try{await c.refresh(d),e=x(i),f=await y(t,s,l,u,i)}catch(e){throw e instanceof a.SessionSupersededError?e:(await b(f,l,d),await h(f,l))}f.status===401&&await b(f,l,e)}else await b(f,l,d)}if(!f.ok)throw await h(f,l);return r(f)}async function C(e,t,n){let{retry:r,...i}=t,a=u(r===void 0?c.retry:r);if(!a)return S(e,i,n);let s=(i.method??`GET`).toUpperCase();return o.retry(()=>S(e,i,n),{...a,shouldRetry:a.shouldRetry??(e=>l(e,s))})}async function w(e,t={}){return C(e,t,i.decodeByContentType)}async function T(e,t,n=`POST`,r){return w(e,{...r,method:n,body:t})}return{request:w,get:(e,t)=>w(e,{...t,method:`GET`}),post:(e,t)=>w(e,{...t,method:`POST`}),put:(e,t)=>w(e,{...t,method:`PUT`}),patch:(e,t)=>w(e,{...t,method:`PATCH`}),delete:(e,t)=>w(e,{...t,method:`DELETE`}),blob:(e,t)=>C(e,{...t},e=>e.blob()),arrayBuffer:(e,t)=>C(e,{...t},e=>e.arrayBuffer()),upload:T}}exports.createApiClient=g;
|
|
1
|
+
const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./credential-scope.cjs"),r=require("./csrf.cjs"),i=require("./decode-response.cjs"),a=require("./errors.cjs"),o=require("./retry.cjs"),s=require("./timeout.cjs");var c=new Set([`GET`,`HEAD`,`OPTIONS`]);function l(e,t){return!c.has(t)||!(e instanceof a.TempestApiError)?!1:a.isRetriableStatus(e.status)}function u(e){return e?e===!0?{}:e:null}var d=15e3,f=3e5;function p(e){return typeof FormData<`u`&&e instanceof FormData}function m(e){return Math.round(performance.now()-e)}async function h(e,t){let n;try{n=await e.clone().json()}catch{try{n=await e.text()}catch{n=null}}return new a.TempestApiError(a.buildApiError(e.status,n,e.headers,t))}function g(c){let g=c.fetcher??globalThis.fetch.bind(globalThis);function _(e,t){if(t)return{};let r=c.getToken?.();return r?n.isTrustedCredentialTarget(e,c.baseURL,c.trustedOrigins)?{Authorization:`Bearer ${r}`}:(n.reportSuppressedCredential(e,c.baseURL),{}):{}}async function v(e,n,i,o){let{body:l,params:u,headers:m,signal:h,timeout:v,...y}=n,b=p(l),x=b?c.uploadTimeout:c.timeout,S=v===void 0?x===void 0?b?f:d:x:v,C=t.buildApiUrl(c.baseURL,e,{prefix:c.prefix,params:u}),w=m,T={...b?{}:{"Content-Type":`application/json`},...i?{"X-Request-ID":i}:{},...c.headers,..._(C,o),...r.csrfHeaders({method:y.method??`GET`,url:C,reference:c.baseURL,csrf:c.csrf,trustedOrigins:c.trustedOrigins,headers:{...c.headers,...w}}),...w},E=s.withTimeout(h,S),D={...y,signal:E.signal,headers:T,credentials:c.withCredentials?`include`:y.credentials,body:l==null?void 0:b?l:JSON.stringify(l)};try{return await g(C,D)}catch(e){throw E.timedOut()?new a.TempestApiError({status:0,detail:`A requisição excedeu ${S}ms e foi abandonada.`}):e}finally{E.dispose()}}async function y(e,t,n,r,i){let a=c.logger;if(!a)return v(e,t,n,i);let o=performance.now();try{let s=await v(e,t,n,i),c={requestId:n,status:s.status,ms:m(o)},l=`${r} ${e} → ${s.status}`;return s.status>=400?a.warn(l,c):a.debug(l,c),s}catch(t){throw a.warn(`${r} ${e} → no response`,{requestId:n,ms:m(o),error:t}),t}}async function b(e,t,n){if(c.onUnauthorized){c.logger?.warn(`unauthorized — calling onUnauthorized`,{requestId:t,status:e.status});try{await c.onUnauthorized(e,n)}catch(n){if(n instanceof a.SessionSupersededError)throw n;c.logger?.warn(`onUnauthorized threw — keeping the original response error`,{requestId:t,status:e.status,error:n})}}}function x(e){return{token:e?null:c.getToken?.()??null,epoch:c.getSessionEpoch?.()??null}}async function S(t,n,r){let{skipAuth:i=!1,skipAuthRetry:o=!1,...s}=n,l=c.requestId?c.requestId():e.randomId(),u=(s.method??`GET`).toUpperCase(),d=x(i),f=await y(t,s,l,u,i);if(f.status===401&&!i&&!o){if(c.refresh){let e;try{await c.refresh(d),e=x(i),f=await y(t,s,l,u,i)}catch(e){throw e instanceof a.SessionSupersededError?e:(await b(f,l,d),await h(f,l))}f.status===401&&await b(f,l,e)}else await b(f,l,d)}if(!f.ok)throw await h(f,l);return r(f)}async function C(e,t,n){let{retry:r,...i}=t,a=u(r===void 0?c.retry:r);if(!a)return S(e,i,n);let s=(i.method??`GET`).toUpperCase();return o.retry(()=>S(e,i,n),{...a,shouldRetry:a.shouldRetry??(e=>l(e,s))})}async function w(e,t={}){return C(e,t,i.decodeByContentType)}async function T(e,t,n=`POST`,r){return w(e,{...r,method:n,body:t})}return{request:w,get:(e,t)=>w(e,{...t,method:`GET`}),post:(e,t)=>w(e,{...t,method:`POST`}),put:(e,t)=>w(e,{...t,method:`PUT`}),patch:(e,t)=>w(e,{...t,method:`PATCH`}),delete:(e,t)=>w(e,{...t,method:`DELETE`}),blob:(e,t)=>C(e,{...t},e=>e.blob()),arrayBuffer:(e,t)=>C(e,{...t},e=>e.arrayBuffer()),upload:T,url:(e,n)=>t.buildApiUrl(c.baseURL,e,{prefix:c.prefix,params:n})}}exports.createApiClient=g;
|
|
2
2
|
//# sourceMappingURL=api-client.cjs.map
|