api-def 0.13.0 → 0.14.0-alpha.2

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/cjs/ApiTypes.d.ts CHANGED
@@ -7,7 +7,8 @@ export type AcceptableStatus = number | [min: number, max: number];
7
7
  export type RawHeaders = Record<string, string | number | boolean | null | undefined>;
8
8
  export type Params = string;
9
9
  export type Query = string | undefined | Record<string, any>;
10
- export type Body = string | number | Record<string, any>;
10
+ export type Body = string | number | Record<string, any> | FormData;
11
+ export type RequestBodyEncoding = "application/json" | "multipart/form-data" | "application/x-www-form-urlencoded";
11
12
  export type State = Record<string, any>;
12
13
  export interface ApiResponse<T = any> {
13
14
  readonly method: RequestMethod;
@@ -3,6 +3,7 @@ import type { Api } from "./Api";
3
3
  import type { ResponseType } from "./ApiConstants";
4
4
  import type { Body, Params, Query, RawHeaders, State } from "./ApiTypes";
5
5
  import Endpoint, { type EndpointOptions } from "./Endpoint";
6
+ import type { BodyValidationOptions, ValidationOptions } from "./Validation";
6
7
  type DefaultResponseOf<T extends ResponseType | undefined> = T extends "text" ? string : T extends "arraybuffer" ? ArrayBuffer : "stream" extends T ? AsyncIterable<Uint8Array> : unknown;
7
8
  export type EndpointBuildOptions<TResponse = unknown, TParams extends Params | undefined = undefined, TQuery extends Query | undefined = undefined, TBody extends Body | undefined = undefined, TState extends State = State, TRequestHeaders extends RawHeaders | undefined = RawHeaders | undefined, TResponseHeaders extends RawHeaders | undefined = RawHeaders | undefined, TPath extends string = string, TResponseType extends ResponseType | undefined = undefined> = Omit<EndpointOptions<TResponse, TParams, TQuery, TBody, TState, TPath, TRequestHeaders, TResponseHeaders>, "validation"> & {
8
9
  responseType?: TResponseType;
@@ -11,10 +12,26 @@ export default class EndpointBuilder<TResponse = unknown, TParams extends Params
11
12
  private api;
12
13
  private readonly validation;
13
14
  constructor(api: Api);
15
+ queryOf<TNewQuery extends Query>(options?: ValidationOptions<TNewQuery>): EndpointBuilder<TResponse, TParams, TNewQuery, TBody, TState, TRequestHeaders, TResponseHeaders>;
16
+ /**
17
+ * @deprecated Pass `{ schema }` instead.
18
+ */
14
19
  queryOf<TNewQuery extends Query>(schema?: zod.Schema<TNewQuery>): EndpointBuilder<TResponse, TParams, TNewQuery, TBody, TState, TRequestHeaders, TResponseHeaders>;
15
20
  paramsOf<TNewParams extends Params>(): EndpointBuilder<TResponse, TNewParams, TQuery, TBody, TState, TRequestHeaders, TResponseHeaders>;
21
+ bodyOf<TNewBody extends Body>(options?: BodyValidationOptions<TNewBody>): EndpointBuilder<TResponse, TParams, TQuery, TNewBody, TState, TRequestHeaders, TResponseHeaders>;
22
+ /**
23
+ * @deprecated Pass `{ schema }` instead.
24
+ */
16
25
  bodyOf<TNewBody extends Body>(schema?: zod.Schema<TNewBody>): EndpointBuilder<TResponse, TParams, TQuery, TNewBody, TState, TRequestHeaders, TResponseHeaders>;
26
+ responseOf<TNewResponse>(options?: ValidationOptions<TNewResponse>): EndpointBuilder<TNewResponse, TParams, TQuery, TBody, TState, TRequestHeaders, TResponseHeaders>;
27
+ /**
28
+ * @deprecated Pass `{ schema }` instead.
29
+ */
17
30
  responseOf<TNewResponse>(schema?: zod.Schema<TNewResponse>): EndpointBuilder<TNewResponse, TParams, TQuery, TBody, TState, TRequestHeaders, TResponseHeaders>;
31
+ stateOf<TNewState extends State>(options?: ValidationOptions<TNewState>): EndpointBuilder<TResponse, TParams, TQuery, TBody, TNewState, TRequestHeaders, TResponseHeaders>;
32
+ /**
33
+ * @deprecated Pass `{ schema }` instead.
34
+ */
18
35
  stateOf<TNewState extends State>(schema?: zod.Schema<TNewState>): EndpointBuilder<TResponse, TParams, TQuery, TBody, TNewState, TRequestHeaders, TResponseHeaders>;
19
36
  requestHeadersOf<TNewRequestHeaders extends RawHeaders>(): EndpointBuilder<TResponse, TParams, TQuery, TBody, TState, TNewRequestHeaders, TResponseHeaders>;
20
37
  responseHeadersOf<TNewResponseHeaders extends RawHeaders>(): EndpointBuilder<TResponse, TParams, TQuery, TBody, TState, TRequestHeaders, TNewResponseHeaders>;
@@ -4,28 +4,56 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const Endpoint_1 = __importDefault(require("./Endpoint"));
7
+ const isSchema = (value) => {
8
+ return value !== undefined && "parse" in value;
9
+ };
10
+ const warnDeprecatedSchemaArgument = (methodName) => {
11
+ console.warn(`[api-def] ${methodName}(schema) is deprecated. Use ${methodName}({ schema }) instead.`);
12
+ };
7
13
  class EndpointBuilder {
8
14
  constructor(api) {
9
15
  this.validation = {};
10
16
  this.api = api;
11
17
  }
12
- queryOf(schema) {
13
- this.validation.query = schema;
18
+ queryOf(optionsOrSchema) {
19
+ if (optionsOrSchema && isSchema(optionsOrSchema)) {
20
+ warnDeprecatedSchemaArgument("queryOf");
21
+ this.validation.query = optionsOrSchema;
22
+ return this;
23
+ }
24
+ this.validation.query = optionsOrSchema?.schema;
14
25
  return this;
15
26
  }
16
27
  paramsOf() {
17
28
  return this;
18
29
  }
19
- bodyOf(schema) {
20
- this.validation.body = schema;
30
+ bodyOf(optionsOrSchema) {
31
+ if (optionsOrSchema && isSchema(optionsOrSchema)) {
32
+ warnDeprecatedSchemaArgument("bodyOf");
33
+ this.validation.body = optionsOrSchema;
34
+ this.validation.bodyEncoding = "application/json";
35
+ return this;
36
+ }
37
+ this.validation.body = optionsOrSchema?.schema;
38
+ this.validation.bodyEncoding = optionsOrSchema?.encoding ?? "application/json";
21
39
  return this;
22
40
  }
23
- responseOf(schema) {
24
- this.validation.response = schema;
41
+ responseOf(optionsOrSchema) {
42
+ if (optionsOrSchema && isSchema(optionsOrSchema)) {
43
+ warnDeprecatedSchemaArgument("responseOf");
44
+ this.validation.response = optionsOrSchema;
45
+ return this;
46
+ }
47
+ this.validation.response = optionsOrSchema?.schema;
25
48
  return this;
26
49
  }
27
- stateOf(schema) {
28
- this.validation.state = schema;
50
+ stateOf(optionsOrSchema) {
51
+ if (optionsOrSchema && isSchema(optionsOrSchema)) {
52
+ warnDeprecatedSchemaArgument("stateOf");
53
+ this.validation.state = optionsOrSchema;
54
+ return this;
55
+ }
56
+ this.validation.state = optionsOrSchema?.schema;
29
57
  return this;
30
58
  }
31
59
  requestHeadersOf() {
@@ -42,6 +42,7 @@ export default class RequestContext<TResponse = any, TParams extends Params | un
42
42
  updateParams(params: Partial<Record<string, string>>): this;
43
43
  private parseRequestBody;
44
44
  getParsedBody(): any;
45
+ parseBody(): void;
45
46
  updateQuery(newQuery: Partial<TQuery>): this;
46
47
  triggerEvent(eventType: RequestEvent): Promise<EventResult<TResponse> | undefined>;
47
48
  addCanceller(canceler: () => void): void;
@@ -129,6 +129,10 @@ class RequestContext {
129
129
  if (this.requestConfig.body && this.requestConfig.headers) {
130
130
  const contentTypeKey = Object.keys(this.requestConfig.headers).find((key) => key.toLowerCase() === "content-type");
131
131
  const contentType = contentTypeKey && this.requestConfig.headers[contentTypeKey]?.toString().split(";")[0].trim();
132
+ const bodyEncoding = this.validation.bodyEncoding ?? "application/json";
133
+ if (!contentTypeKey && bodyEncoding === "application/x-www-form-urlencoded") {
134
+ this.requestConfig.headers["Content-Type"] = "application/x-www-form-urlencoded";
135
+ }
132
136
  if (contentType === "application/x-www-form-urlencoded") {
133
137
  const searchParams = new URLSearchParams();
134
138
  for (const key of Object.keys(this.requestConfig.body)) {
@@ -137,6 +141,17 @@ class RequestContext {
137
141
  }
138
142
  this.parsedBody = searchParams;
139
143
  }
144
+ else if (bodyEncoding === "application/x-www-form-urlencoded") {
145
+ const searchParams = new URLSearchParams();
146
+ for (const key of Object.keys(this.requestConfig.body)) {
147
+ const value = this.requestConfig.body[key];
148
+ searchParams.append(key, value?.toString());
149
+ }
150
+ this.parsedBody = searchParams;
151
+ }
152
+ else if (bodyEncoding === "multipart/form-data") {
153
+ this.parsedBody = Utils.toFormData(this.requestConfig.body);
154
+ }
140
155
  else {
141
156
  this.parsedBody = this.requestConfig.body;
142
157
  }
@@ -148,6 +163,9 @@ class RequestContext {
148
163
  getParsedBody() {
149
164
  return this.parsedBody;
150
165
  }
166
+ parseBody() {
167
+ this.parseRequestBody();
168
+ }
151
169
  updateQuery(newQuery) {
152
170
  this.requestConfig.queryObject = Utils.assign({}, this.requestConfig.queryObject, newQuery);
153
171
  this.computedRequestUrl = this.resolveRequestUrl();
package/cjs/Requester.js CHANGED
@@ -95,6 +95,7 @@ const makeRequest = async (context) => {
95
95
  });
96
96
  }
97
97
  }
98
+ context.parseBody();
98
99
  const retryOptions = parseRetryOptions(context.requestConfig?.retry);
99
100
  const internalRetryOptions = {
100
101
  retries: retryOptions.maxAttempts,
package/cjs/Utils.d.ts CHANGED
@@ -14,3 +14,4 @@ export declare const noop: () => void;
14
14
  export declare const delayThenReturn: <T>(value: T, delayMs: number) => Promise<T>;
15
15
  export declare const randInt: (min: number, max: number) => number;
16
16
  export declare const isFormData: (value: any) => value is FormData;
17
+ export declare const toFormData: (body: any) => FormData;
package/cjs/Utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isFormData = exports.randInt = exports.delayThenReturn = exports.noop = exports.getGlobalFetch = exports.getGlobal = exports.padNumber = exports.assign = void 0;
3
+ exports.toFormData = exports.isFormData = exports.randInt = exports.delayThenReturn = exports.noop = exports.getGlobalFetch = exports.getGlobal = exports.padNumber = exports.assign = void 0;
4
4
  // polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
5
5
  const hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
6
6
  exports.assign = Object.assign ||
@@ -73,6 +73,58 @@ const randInt = (min, max) => {
73
73
  };
74
74
  exports.randInt = randInt;
75
75
  const isFormData = (value) => {
76
- return value instanceof FormData;
76
+ return typeof FormData !== "undefined" && value instanceof FormData;
77
77
  };
78
78
  exports.isFormData = isFormData;
79
+ const isPlainObject = (value) => {
80
+ return Object.prototype.toString.call(value) === "[object Object]";
81
+ };
82
+ const isBlob = (value) => {
83
+ return typeof Blob !== "undefined" && value instanceof Blob;
84
+ };
85
+ const isArrayBuffer = (value) => {
86
+ return typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer;
87
+ };
88
+ const isArrayBufferView = (value) => {
89
+ return typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value);
90
+ };
91
+ const toFormDataValue = (value) => {
92
+ if (isBlob(value)) {
93
+ return value;
94
+ }
95
+ if ((isArrayBuffer(value) || isArrayBufferView(value)) && typeof Blob !== "undefined") {
96
+ return new Blob([value]);
97
+ }
98
+ return String(value);
99
+ };
100
+ const toFormData = (body) => {
101
+ if ((0, exports.isFormData)(body)) {
102
+ return body;
103
+ }
104
+ const formData = new FormData();
105
+ const appendValue = (key, value) => {
106
+ if (value === undefined || value === null) {
107
+ return;
108
+ }
109
+ if (Array.isArray(value)) {
110
+ for (const [index, item] of value.entries()) {
111
+ appendValue(`${key}.${index}`, item);
112
+ }
113
+ return;
114
+ }
115
+ if (isPlainObject(value) && !isBlob(value)) {
116
+ for (const nestedKey of Object.keys(value)) {
117
+ appendValue(`${key}.${nestedKey}`, value[nestedKey]);
118
+ }
119
+ return;
120
+ }
121
+ formData.append(key, toFormDataValue(value));
122
+ };
123
+ if (isPlainObject(body)) {
124
+ for (const key of Object.keys(body)) {
125
+ appendValue(key, body[key]);
126
+ }
127
+ }
128
+ return formData;
129
+ };
130
+ exports.toFormData = toFormData;
@@ -1,9 +1,16 @@
1
1
  import type * as zod from "zod";
2
- import type { Body, Params, Query, State } from "./ApiTypes";
2
+ import type { Body, Params, Query, RequestBodyEncoding, State } from "./ApiTypes";
3
+ export interface ValidationOptions<TValue> {
4
+ schema?: zod.Schema<TValue>;
5
+ }
6
+ export interface BodyValidationOptions<TBody extends Body> extends ValidationOptions<TBody> {
7
+ encoding?: RequestBodyEncoding;
8
+ }
3
9
  export interface Validation<TResponse = any, TParams extends Params | undefined = Params | undefined, TQuery extends Query | undefined = Query | undefined, TBody extends Body | undefined = Body | undefined, TState extends State = State> {
4
10
  query?: zod.Schema<TQuery>;
5
11
  params?: zod.Schema<TParams>;
6
12
  body?: zod.Schema<TBody>;
13
+ bodyEncoding?: RequestBodyEncoding;
7
14
  response?: zod.Schema<TResponse>;
8
15
  state?: zod.Schema<TState>;
9
16
  }
@@ -111,7 +111,7 @@ class FetchRequestBackend {
111
111
  let softAbort = false;
112
112
  let responded = false;
113
113
  const body = context.getParsedBody();
114
- const bodyJsonify = body !== null && typeof body === "object" && !Utils.isFormData(body);
114
+ const bodyJsonify = body !== null && typeof body === "object" && !Utils.isFormData(body) && !(body instanceof URLSearchParams);
115
115
  const headers = Utils.assign({
116
116
  // logic from axios
117
117
  "Content-Type": bodyJsonify ? "application/json;charset=utf-8" : undefined,
package/esm/ApiTypes.d.ts CHANGED
@@ -7,7 +7,8 @@ export type AcceptableStatus = number | [min: number, max: number];
7
7
  export type RawHeaders = Record<string, string | number | boolean | null | undefined>;
8
8
  export type Params = string;
9
9
  export type Query = string | undefined | Record<string, any>;
10
- export type Body = string | number | Record<string, any>;
10
+ export type Body = string | number | Record<string, any> | FormData;
11
+ export type RequestBodyEncoding = "application/json" | "multipart/form-data" | "application/x-www-form-urlencoded";
11
12
  export type State = Record<string, any>;
12
13
  export interface ApiResponse<T = any> {
13
14
  readonly method: RequestMethod;
@@ -3,6 +3,7 @@ import type { Api } from "./Api.js";
3
3
  import type { ResponseType } from "./ApiConstants.js";
4
4
  import type { Body, Params, Query, RawHeaders, State } from "./ApiTypes.js";
5
5
  import Endpoint, { type EndpointOptions } from "./Endpoint.js";
6
+ import type { BodyValidationOptions, ValidationOptions } from "./Validation.js";
6
7
  type DefaultResponseOf<T extends ResponseType | undefined> = T extends "text" ? string : T extends "arraybuffer" ? ArrayBuffer : "stream" extends T ? AsyncIterable<Uint8Array> : unknown;
7
8
  export type EndpointBuildOptions<TResponse = unknown, TParams extends Params | undefined = undefined, TQuery extends Query | undefined = undefined, TBody extends Body | undefined = undefined, TState extends State = State, TRequestHeaders extends RawHeaders | undefined = RawHeaders | undefined, TResponseHeaders extends RawHeaders | undefined = RawHeaders | undefined, TPath extends string = string, TResponseType extends ResponseType | undefined = undefined> = Omit<EndpointOptions<TResponse, TParams, TQuery, TBody, TState, TPath, TRequestHeaders, TResponseHeaders>, "validation"> & {
8
9
  responseType?: TResponseType;
@@ -11,10 +12,26 @@ export default class EndpointBuilder<TResponse = unknown, TParams extends Params
11
12
  private api;
12
13
  private readonly validation;
13
14
  constructor(api: Api);
15
+ queryOf<TNewQuery extends Query>(options?: ValidationOptions<TNewQuery>): EndpointBuilder<TResponse, TParams, TNewQuery, TBody, TState, TRequestHeaders, TResponseHeaders>;
16
+ /**
17
+ * @deprecated Pass `{ schema }` instead.
18
+ */
14
19
  queryOf<TNewQuery extends Query>(schema?: zod.Schema<TNewQuery>): EndpointBuilder<TResponse, TParams, TNewQuery, TBody, TState, TRequestHeaders, TResponseHeaders>;
15
20
  paramsOf<TNewParams extends Params>(): EndpointBuilder<TResponse, TNewParams, TQuery, TBody, TState, TRequestHeaders, TResponseHeaders>;
21
+ bodyOf<TNewBody extends Body>(options?: BodyValidationOptions<TNewBody>): EndpointBuilder<TResponse, TParams, TQuery, TNewBody, TState, TRequestHeaders, TResponseHeaders>;
22
+ /**
23
+ * @deprecated Pass `{ schema }` instead.
24
+ */
16
25
  bodyOf<TNewBody extends Body>(schema?: zod.Schema<TNewBody>): EndpointBuilder<TResponse, TParams, TQuery, TNewBody, TState, TRequestHeaders, TResponseHeaders>;
26
+ responseOf<TNewResponse>(options?: ValidationOptions<TNewResponse>): EndpointBuilder<TNewResponse, TParams, TQuery, TBody, TState, TRequestHeaders, TResponseHeaders>;
27
+ /**
28
+ * @deprecated Pass `{ schema }` instead.
29
+ */
17
30
  responseOf<TNewResponse>(schema?: zod.Schema<TNewResponse>): EndpointBuilder<TNewResponse, TParams, TQuery, TBody, TState, TRequestHeaders, TResponseHeaders>;
31
+ stateOf<TNewState extends State>(options?: ValidationOptions<TNewState>): EndpointBuilder<TResponse, TParams, TQuery, TBody, TNewState, TRequestHeaders, TResponseHeaders>;
32
+ /**
33
+ * @deprecated Pass `{ schema }` instead.
34
+ */
18
35
  stateOf<TNewState extends State>(schema?: zod.Schema<TNewState>): EndpointBuilder<TResponse, TParams, TQuery, TBody, TNewState, TRequestHeaders, TResponseHeaders>;
19
36
  requestHeadersOf<TNewRequestHeaders extends RawHeaders>(): EndpointBuilder<TResponse, TParams, TQuery, TBody, TState, TNewRequestHeaders, TResponseHeaders>;
20
37
  responseHeadersOf<TNewResponseHeaders extends RawHeaders>(): EndpointBuilder<TResponse, TParams, TQuery, TBody, TState, TRequestHeaders, TNewResponseHeaders>;
@@ -1,26 +1,54 @@
1
1
  import Endpoint from "./Endpoint.js";
2
+ const isSchema = (value) => {
3
+ return value !== undefined && "parse" in value;
4
+ };
5
+ const warnDeprecatedSchemaArgument = (methodName) => {
6
+ console.warn(`[api-def] ${methodName}(schema) is deprecated. Use ${methodName}({ schema }) instead.`);
7
+ };
2
8
  export default class EndpointBuilder {
3
9
  constructor(api) {
4
10
  this.validation = {};
5
11
  this.api = api;
6
12
  }
7
- queryOf(schema) {
8
- this.validation.query = schema;
13
+ queryOf(optionsOrSchema) {
14
+ if (optionsOrSchema && isSchema(optionsOrSchema)) {
15
+ warnDeprecatedSchemaArgument("queryOf");
16
+ this.validation.query = optionsOrSchema;
17
+ return this;
18
+ }
19
+ this.validation.query = optionsOrSchema?.schema;
9
20
  return this;
10
21
  }
11
22
  paramsOf() {
12
23
  return this;
13
24
  }
14
- bodyOf(schema) {
15
- this.validation.body = schema;
25
+ bodyOf(optionsOrSchema) {
26
+ if (optionsOrSchema && isSchema(optionsOrSchema)) {
27
+ warnDeprecatedSchemaArgument("bodyOf");
28
+ this.validation.body = optionsOrSchema;
29
+ this.validation.bodyEncoding = "application/json";
30
+ return this;
31
+ }
32
+ this.validation.body = optionsOrSchema?.schema;
33
+ this.validation.bodyEncoding = optionsOrSchema?.encoding ?? "application/json";
16
34
  return this;
17
35
  }
18
- responseOf(schema) {
19
- this.validation.response = schema;
36
+ responseOf(optionsOrSchema) {
37
+ if (optionsOrSchema && isSchema(optionsOrSchema)) {
38
+ warnDeprecatedSchemaArgument("responseOf");
39
+ this.validation.response = optionsOrSchema;
40
+ return this;
41
+ }
42
+ this.validation.response = optionsOrSchema?.schema;
20
43
  return this;
21
44
  }
22
- stateOf(schema) {
23
- this.validation.state = schema;
45
+ stateOf(optionsOrSchema) {
46
+ if (optionsOrSchema && isSchema(optionsOrSchema)) {
47
+ warnDeprecatedSchemaArgument("stateOf");
48
+ this.validation.state = optionsOrSchema;
49
+ return this;
50
+ }
51
+ this.validation.state = optionsOrSchema?.schema;
24
52
  return this;
25
53
  }
26
54
  requestHeadersOf() {
@@ -42,6 +42,7 @@ export default class RequestContext<TResponse = any, TParams extends Params | un
42
42
  updateParams(params: Partial<Record<string, string>>): this;
43
43
  private parseRequestBody;
44
44
  getParsedBody(): any;
45
+ parseBody(): void;
45
46
  updateQuery(newQuery: Partial<TQuery>): this;
46
47
  triggerEvent(eventType: RequestEvent): Promise<EventResult<TResponse> | undefined>;
47
48
  addCanceller(canceler: () => void): void;
@@ -94,6 +94,10 @@ export default class RequestContext {
94
94
  if (this.requestConfig.body && this.requestConfig.headers) {
95
95
  const contentTypeKey = Object.keys(this.requestConfig.headers).find((key) => key.toLowerCase() === "content-type");
96
96
  const contentType = contentTypeKey && this.requestConfig.headers[contentTypeKey]?.toString().split(";")[0].trim();
97
+ const bodyEncoding = this.validation.bodyEncoding ?? "application/json";
98
+ if (!contentTypeKey && bodyEncoding === "application/x-www-form-urlencoded") {
99
+ this.requestConfig.headers["Content-Type"] = "application/x-www-form-urlencoded";
100
+ }
97
101
  if (contentType === "application/x-www-form-urlencoded") {
98
102
  const searchParams = new URLSearchParams();
99
103
  for (const key of Object.keys(this.requestConfig.body)) {
@@ -102,6 +106,17 @@ export default class RequestContext {
102
106
  }
103
107
  this.parsedBody = searchParams;
104
108
  }
109
+ else if (bodyEncoding === "application/x-www-form-urlencoded") {
110
+ const searchParams = new URLSearchParams();
111
+ for (const key of Object.keys(this.requestConfig.body)) {
112
+ const value = this.requestConfig.body[key];
113
+ searchParams.append(key, value?.toString());
114
+ }
115
+ this.parsedBody = searchParams;
116
+ }
117
+ else if (bodyEncoding === "multipart/form-data") {
118
+ this.parsedBody = Utils.toFormData(this.requestConfig.body);
119
+ }
105
120
  else {
106
121
  this.parsedBody = this.requestConfig.body;
107
122
  }
@@ -113,6 +128,9 @@ export default class RequestContext {
113
128
  getParsedBody() {
114
129
  return this.parsedBody;
115
130
  }
131
+ parseBody() {
132
+ this.parseRequestBody();
133
+ }
116
134
  updateQuery(newQuery) {
117
135
  this.requestConfig.queryObject = Utils.assign({}, this.requestConfig.queryObject, newQuery);
118
136
  this.computedRequestUrl = this.resolveRequestUrl();
package/esm/Requester.js CHANGED
@@ -88,6 +88,7 @@ const makeRequest = async (context) => {
88
88
  });
89
89
  }
90
90
  }
91
+ context.parseBody();
91
92
  const retryOptions = parseRetryOptions(context.requestConfig?.retry);
92
93
  const internalRetryOptions = {
93
94
  retries: retryOptions.maxAttempts,
package/esm/Utils.d.ts CHANGED
@@ -14,3 +14,4 @@ export declare const noop: () => void;
14
14
  export declare const delayThenReturn: <T>(value: T, delayMs: number) => Promise<T>;
15
15
  export declare const randInt: (min: number, max: number) => number;
16
16
  export declare const isFormData: (value: any) => value is FormData;
17
+ export declare const toFormData: (body: any) => FormData;
package/esm/Utils.js CHANGED
@@ -64,5 +64,56 @@ export const randInt = (min, max) => {
64
64
  return Math.floor(Math.random() * (maxI - minI + 1)) + minI;
65
65
  };
66
66
  export const isFormData = (value) => {
67
- return value instanceof FormData;
67
+ return typeof FormData !== "undefined" && value instanceof FormData;
68
+ };
69
+ const isPlainObject = (value) => {
70
+ return Object.prototype.toString.call(value) === "[object Object]";
71
+ };
72
+ const isBlob = (value) => {
73
+ return typeof Blob !== "undefined" && value instanceof Blob;
74
+ };
75
+ const isArrayBuffer = (value) => {
76
+ return typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer;
77
+ };
78
+ const isArrayBufferView = (value) => {
79
+ return typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value);
80
+ };
81
+ const toFormDataValue = (value) => {
82
+ if (isBlob(value)) {
83
+ return value;
84
+ }
85
+ if ((isArrayBuffer(value) || isArrayBufferView(value)) && typeof Blob !== "undefined") {
86
+ return new Blob([value]);
87
+ }
88
+ return String(value);
89
+ };
90
+ export const toFormData = (body) => {
91
+ if (isFormData(body)) {
92
+ return body;
93
+ }
94
+ const formData = new FormData();
95
+ const appendValue = (key, value) => {
96
+ if (value === undefined || value === null) {
97
+ return;
98
+ }
99
+ if (Array.isArray(value)) {
100
+ for (const [index, item] of value.entries()) {
101
+ appendValue(`${key}.${index}`, item);
102
+ }
103
+ return;
104
+ }
105
+ if (isPlainObject(value) && !isBlob(value)) {
106
+ for (const nestedKey of Object.keys(value)) {
107
+ appendValue(`${key}.${nestedKey}`, value[nestedKey]);
108
+ }
109
+ return;
110
+ }
111
+ formData.append(key, toFormDataValue(value));
112
+ };
113
+ if (isPlainObject(body)) {
114
+ for (const key of Object.keys(body)) {
115
+ appendValue(key, body[key]);
116
+ }
117
+ }
118
+ return formData;
68
119
  };
@@ -1,9 +1,16 @@
1
1
  import type * as zod from "zod";
2
- import type { Body, Params, Query, State } from "./ApiTypes.js";
2
+ import type { Body, Params, Query, RequestBodyEncoding, State } from "./ApiTypes.js";
3
+ export interface ValidationOptions<TValue> {
4
+ schema?: zod.Schema<TValue>;
5
+ }
6
+ export interface BodyValidationOptions<TBody extends Body> extends ValidationOptions<TBody> {
7
+ encoding?: RequestBodyEncoding;
8
+ }
3
9
  export interface Validation<TResponse = any, TParams extends Params | undefined = Params | undefined, TQuery extends Query | undefined = Query | undefined, TBody extends Body | undefined = Body | undefined, TState extends State = State> {
4
10
  query?: zod.Schema<TQuery>;
5
11
  params?: zod.Schema<TParams>;
6
12
  body?: zod.Schema<TBody>;
13
+ bodyEncoding?: RequestBodyEncoding;
7
14
  response?: zod.Schema<TResponse>;
8
15
  state?: zod.Schema<TState>;
9
16
  }
@@ -76,7 +76,7 @@ export default class FetchRequestBackend {
76
76
  let softAbort = false;
77
77
  let responded = false;
78
78
  const body = context.getParsedBody();
79
- const bodyJsonify = body !== null && typeof body === "object" && !Utils.isFormData(body);
79
+ const bodyJsonify = body !== null && typeof body === "object" && !Utils.isFormData(body) && !(body instanceof URLSearchParams);
80
80
  const headers = Utils.assign({
81
81
  // logic from axios
82
82
  "Content-Type": bodyJsonify ? "application/json;charset=utf-8" : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-def",
3
- "version": "0.13.0",
3
+ "version": "0.14.0-alpha.2",
4
4
  "description": "Typed API definitions with middleware support",
5
5
  "main": "cjs/index.js",
6
6
  "types": "esm/index.d.ts",
@@ -91,6 +91,7 @@
91
91
  "commander": "^15.0.0",
92
92
  "cross-env": "10.1.0",
93
93
  "esbuild": "^0.28.0",
94
+ "formdata-node": "6.0.3",
94
95
  "jest": "30.4.2",
95
96
  "kleur": "^4.1.5",
96
97
  "lodash": "^4.18.1",