@vahidkaargar/customized-api-client 0.2.3 → 0.3.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/dist/index.d.ts CHANGED
@@ -16,6 +16,11 @@ interface RetryOptions {
16
16
  readonly baseDelayMs?: number;
17
17
  readonly maxDelayMs?: number;
18
18
  readonly jitterRatio?: number;
19
+ /**
20
+ * When true, POST/PUT/PATCH/DELETE responses in the **5xx** range are retried like GET/HEAD
21
+ * (same `AxiosRequestConfig`, so same `Idempotency-Key` and body). Default false.
22
+ */
23
+ readonly retryMutationsOnServerError?: boolean;
19
24
  }
20
25
  interface IdempotencyReplayContext {
21
26
  readonly url?: string;
@@ -119,6 +124,9 @@ interface MultiStatusBody {
119
124
  readonly headers: NormalizedResponseHeaders;
120
125
  }
121
126
  type ClientSuccess = JsonApiSuccessBody | NoContentBody | AcceptedBody | MultiStatusBody;
127
+ type ClientSuccessWithDocument<T extends JsonApiDocument = JsonApiDocument> = (JsonApiSuccessBody & {
128
+ readonly document: T;
129
+ }) | NoContentBody | AcceptedBody | MultiStatusBody;
122
130
  interface OkResult<T extends ClientSuccess> {
123
131
  readonly ok: true;
124
132
  readonly value: T;
@@ -149,35 +157,44 @@ interface RequestCallOptions {
149
157
  readonly signal?: AbortSignal;
150
158
  }
151
159
  interface ApiClient {
152
- readonly get: (path: string, opts?: RequestCallOptions) => Promise<ClientSuccess>;
153
- readonly head: (path: string, opts?: RequestCallOptions) => Promise<ClientSuccess>;
154
- readonly post: (path: string, data?: unknown, opts?: RequestCallOptions) => Promise<ClientSuccess>;
155
- readonly patch: (path: string, data?: unknown, opts?: RequestCallOptions) => Promise<ClientSuccess>;
156
- readonly put: (path: string, data?: unknown, opts?: RequestCallOptions) => Promise<ClientSuccess>;
157
- readonly delete: (path: string, opts?: RequestCallOptions) => Promise<ClientSuccess>;
158
- readonly request: (ax: AxiosRequestConfig, opts?: RequestCallOptions) => Promise<ClientSuccess>;
159
- readonly getByUrl: (fullUrl: string, opts?: RequestCallOptions) => Promise<ClientSuccess>;
160
- readonly patchWithVersion: (path: string, data: unknown, version: number, opts?: Omit<RequestCallOptions, 'ifMatchVersion'>) => Promise<ClientSuccess>;
161
- readonly safeGet: (path: string, opts?: RequestCallOptions) => Promise<Result<ClientSuccess, ApiClientError>>;
162
- readonly safePost: (path: string, data?: unknown, opts?: RequestCallOptions) => Promise<Result<ClientSuccess, ApiClientError>>;
163
- readonly safePatch: (path: string, data?: unknown, opts?: RequestCallOptions) => Promise<Result<ClientSuccess, ApiClientError>>;
164
- readonly safePut: (path: string, data?: unknown, opts?: RequestCallOptions) => Promise<Result<ClientSuccess, ApiClientError>>;
165
- readonly safeDelete: (path: string, opts?: RequestCallOptions) => Promise<Result<ClientSuccess, ApiClientError>>;
166
- readonly safeHead: (path: string, opts?: RequestCallOptions) => Promise<Result<ClientSuccess, ApiClientError>>;
167
- readonly safeRequest: (ax: AxiosRequestConfig, opts?: RequestCallOptions) => Promise<Result<ClientSuccess, ApiClientError>>;
168
- readonly safeGetByUrl: (fullUrl: string, opts?: RequestCallOptions) => Promise<Result<ClientSuccess, ApiClientError>>;
169
- readonly safePatchWithVersion: (path: string, data: unknown, version: number, opts?: Omit<RequestCallOptions, 'ifMatchVersion'>) => Promise<Result<ClientSuccess, ApiClientError>>;
160
+ readonly get: <T extends JsonApiDocument = JsonApiDocument>(path: string, opts?: RequestCallOptions) => Promise<ClientSuccessWithDocument<T>>;
161
+ readonly head: <T extends JsonApiDocument = JsonApiDocument>(path: string, opts?: RequestCallOptions) => Promise<ClientSuccessWithDocument<T>>;
162
+ readonly post: <T extends JsonApiDocument = JsonApiDocument>(path: string, data?: unknown, opts?: RequestCallOptions) => Promise<ClientSuccessWithDocument<T>>;
163
+ readonly patch: <T extends JsonApiDocument = JsonApiDocument>(path: string, data?: unknown, opts?: RequestCallOptions) => Promise<ClientSuccessWithDocument<T>>;
164
+ readonly put: <T extends JsonApiDocument = JsonApiDocument>(path: string, data?: unknown, opts?: RequestCallOptions) => Promise<ClientSuccessWithDocument<T>>;
165
+ readonly delete: <T extends JsonApiDocument = JsonApiDocument>(path: string, opts?: RequestCallOptions) => Promise<ClientSuccessWithDocument<T>>;
166
+ readonly postFormData: <T extends JsonApiDocument = JsonApiDocument>(path: string, data: FormData, opts?: RequestCallOptions) => Promise<ClientSuccessWithDocument<T>>;
167
+ readonly request: <T extends JsonApiDocument = JsonApiDocument>(ax: AxiosRequestConfig, opts?: RequestCallOptions) => Promise<ClientSuccessWithDocument<T>>;
168
+ readonly getByUrl: <T extends JsonApiDocument = JsonApiDocument>(fullUrl: string, opts?: RequestCallOptions) => Promise<ClientSuccessWithDocument<T>>;
169
+ readonly patchWithVersion: <T extends JsonApiDocument = JsonApiDocument>(path: string, data: unknown, version: number, opts?: Omit<RequestCallOptions, 'ifMatchVersion'>) => Promise<ClientSuccessWithDocument<T>>;
170
+ readonly safeGet: <T extends JsonApiDocument = JsonApiDocument>(path: string, opts?: RequestCallOptions) => Promise<Result<ClientSuccessWithDocument<T>, ApiClientError>>;
171
+ readonly safePost: <T extends JsonApiDocument = JsonApiDocument>(path: string, data?: unknown, opts?: RequestCallOptions) => Promise<Result<ClientSuccessWithDocument<T>, ApiClientError>>;
172
+ readonly safePatch: <T extends JsonApiDocument = JsonApiDocument>(path: string, data?: unknown, opts?: RequestCallOptions) => Promise<Result<ClientSuccessWithDocument<T>, ApiClientError>>;
173
+ readonly safePut: <T extends JsonApiDocument = JsonApiDocument>(path: string, data?: unknown, opts?: RequestCallOptions) => Promise<Result<ClientSuccessWithDocument<T>, ApiClientError>>;
174
+ readonly safeDelete: <T extends JsonApiDocument = JsonApiDocument>(path: string, opts?: RequestCallOptions) => Promise<Result<ClientSuccessWithDocument<T>, ApiClientError>>;
175
+ readonly safeHead: <T extends JsonApiDocument = JsonApiDocument>(path: string, opts?: RequestCallOptions) => Promise<Result<ClientSuccessWithDocument<T>, ApiClientError>>;
176
+ readonly safeRequest: <T extends JsonApiDocument = JsonApiDocument>(ax: AxiosRequestConfig, opts?: RequestCallOptions) => Promise<Result<ClientSuccessWithDocument<T>, ApiClientError>>;
177
+ readonly safeGetByUrl: <T extends JsonApiDocument = JsonApiDocument>(fullUrl: string, opts?: RequestCallOptions) => Promise<Result<ClientSuccessWithDocument<T>, ApiClientError>>;
178
+ readonly safePatchWithVersion: <T extends JsonApiDocument = JsonApiDocument>(path: string, data: unknown, version: number, opts?: Omit<RequestCallOptions, 'ifMatchVersion'>) => Promise<Result<ClientSuccessWithDocument<T>, ApiClientError>>;
170
179
  }
171
180
  declare function createApiClient(config: ApiClientConfig): ApiClient;
172
181
 
182
+ declare function hasErrorCode(error: unknown, code: string): boolean;
183
+ declare function isApiClientErrorWithCode(error: unknown, code: string): error is ApiClientError;
173
184
  declare function isAuthenticationError(e: unknown): boolean;
174
185
  declare function isForbiddenError(e: unknown): boolean;
186
+ /** True for any HTTP 428. Prefer code-specific helpers when branching on `errors[].code`. */
175
187
  declare function isPreconditionRequiredError(e: unknown): boolean;
188
+ declare function isIdempotencyKeyRequiredError(e: unknown): boolean;
189
+ declare function isIfMatchRequiredError(e: unknown): boolean;
190
+ declare function isMfaVerificationRequiredError(e: unknown): boolean;
176
191
  declare function isPreconditionFailedError(e: unknown): boolean;
177
192
  declare function isValidationError(e: unknown): boolean;
178
193
  declare function isConflictError(e: unknown): boolean;
179
194
  declare function isPayloadTooLargeError(e: unknown): boolean;
180
- declare function isRetryablePerPolicy(e: unknown): boolean;
195
+ declare function isRetryablePerPolicy(e: unknown, policy?: Readonly<{
196
+ retryMutationsOnServerError?: boolean;
197
+ }>): boolean;
181
198
 
182
199
  /**
183
200
  * Truncate a value for safe logging (respects `maxBodyLogLength` from config when passed).
@@ -258,6 +275,8 @@ interface RetryPolicyContext {
258
275
  /** First JSON:API error `code` when present */
259
276
  readonly primaryErrorCode?: string;
260
277
  readonly isNetworkError: boolean;
278
+ /** When true, mutations retry on HTTP 5xx (after idempotency 409 rules). */
279
+ readonly retryMutationsOnServerError?: boolean;
261
280
  }
262
281
  /** Pure policy per [.cursor/tasks/project-plan.md §7](../tasks/project-plan.md). */
263
282
  declare function retryAllowed(ctx: RetryPolicyContext): boolean;
@@ -317,4 +336,4 @@ declare function pollAsyncResult(client: ApiClient, initial: Extract<ClientSucce
317
336
 
318
337
  declare const PACKAGE_VERSION: string;
319
338
 
320
- export { type AcceptedBody, type ApiClient, type ApiClientConfig, ApiClientError, type AuthConfig, type BaseUrlMode, type ClientSuccess, type CursorPagination, DEFAULT_PAGE_SIZE_CAP, DEFAULT_TIMEOUT_MS, type DeprecationInfo, type ErrResult, IDEMPOTENCY_MAX_LENGTH, type IdempotencyReplayContext, type IncludedIndex, type JsonApiDocument, type JsonApiErrorDocument, type JsonApiErrorObject, type JsonApiPrimaryData, type JsonApiQueryInput, type JsonApiResourceLinkage, type JsonApiResourceObject, type JsonApiSuccessBody, type MultiStatusBody, type MultiStatusItem, type NoContentBody, type NormalizedResponseHeaders, type OffsetPagination, type OkResult, PACKAGE_VERSION, type PollOptions, type RequestCallOptions, type Result, type RetryOptions, type TokenProvider, type TransformResponseKeysMode, type UnknownPagination, type ValidationGroups, applyJsonApiHeaders, applyTransformKeys, assertValidIdempotencyKey, buildCursorPageParams, buildJsonApiQuery, buildOffsetPageParams, createApiClient, createHealthCheck, defaultIdempotencyKey, dispatchWithRetry, etagFromResponseHeaders, flattenAxiosHeaders, formatIfMatch, getHeader, getNextPageUrl, groupValidationErrorsByPointer, indexIncluded, isApiClientError, isAuthenticationError, isConflictError, isForbiddenError, isMutationMethod, isPayloadTooLargeError, isPreconditionFailedError, isPreconditionRequiredError, isRetryablePerPolicy, isValidationError, normalizeAxiosResponse, normalizeHttpUrl, parseDeprecationHeaders, parseJsonApiDocument, parseJsonApiErrorBody, parseMultiStatusBody, parsePaginationKind, parseRetryAfterSeconds, pollAsyncResult, readResourceVersion, redactHeaderRecord, resolveAcceptLanguage, resolveAcceptedLocation, resolveAuthorizationHeader, resolveIncluded, resolveResourcePath, retryAllowed, truncateForLog };
339
+ export { type AcceptedBody, type ApiClient, type ApiClientConfig, ApiClientError, type AuthConfig, type BaseUrlMode, type ClientSuccess, type ClientSuccessWithDocument, type CursorPagination, DEFAULT_PAGE_SIZE_CAP, DEFAULT_TIMEOUT_MS, type DeprecationInfo, type ErrResult, IDEMPOTENCY_MAX_LENGTH, type IdempotencyReplayContext, type IncludedIndex, type JsonApiDocument, type JsonApiErrorDocument, type JsonApiErrorObject, type JsonApiPrimaryData, type JsonApiQueryInput, type JsonApiResourceLinkage, type JsonApiResourceObject, type JsonApiSuccessBody, type MultiStatusBody, type MultiStatusItem, type NoContentBody, type NormalizedResponseHeaders, type OffsetPagination, type OkResult, PACKAGE_VERSION, type PollOptions, type RequestCallOptions, type Result, type RetryOptions, type TokenProvider, type TransformResponseKeysMode, type UnknownPagination, type ValidationGroups, applyJsonApiHeaders, applyTransformKeys, assertValidIdempotencyKey, buildCursorPageParams, buildJsonApiQuery, buildOffsetPageParams, createApiClient, createHealthCheck, defaultIdempotencyKey, dispatchWithRetry, etagFromResponseHeaders, flattenAxiosHeaders, formatIfMatch, getHeader, getNextPageUrl, groupValidationErrorsByPointer, hasErrorCode, indexIncluded, isApiClientError, isApiClientErrorWithCode, isAuthenticationError, isConflictError, isForbiddenError, isIdempotencyKeyRequiredError, isIfMatchRequiredError, isMfaVerificationRequiredError, isMutationMethod, isPayloadTooLargeError, isPreconditionFailedError, isPreconditionRequiredError, isRetryablePerPolicy, isValidationError, normalizeAxiosResponse, normalizeHttpUrl, parseDeprecationHeaders, parseJsonApiDocument, parseJsonApiErrorBody, parseMultiStatusBody, parsePaginationKind, parseRetryAfterSeconds, pollAsyncResult, readResourceVersion, redactHeaderRecord, resolveAcceptLanguage, resolveAcceptedLocation, resolveAuthorizationHeader, resolveIncluded, resolveResourcePath, retryAllowed, truncateForLog };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "@vahidkaargar/customized-api-client",
4
- version: "0.2.3",
4
+ version: "0.3.0",
5
5
  description: "TypeScript Axios client for JSON:API v1.1 with idempotency, retries, and normalized results",
6
6
  type: "module",
7
7
  engines: {
@@ -87,14 +87,29 @@ function applyJsonApiHeaders(config, method) {
87
87
  const m = method.toUpperCase();
88
88
  const headers = { ...config.headers };
89
89
  headers.Accept = headers.Accept ?? JSON_API;
90
- if (hasJsonBody(m, config)) {
90
+ if (shouldSetJsonApiContentType(m, config)) {
91
91
  headers["Content-Type"] = headers["Content-Type"] ?? JSON_API;
92
92
  }
93
93
  return { ...config, headers };
94
94
  }
95
- function hasJsonBody(method, config) {
95
+ function shouldSetJsonApiContentType(method, config) {
96
96
  if (!["POST", "PATCH", "PUT"].includes(method)) return false;
97
- return config.data !== void 0 && config.data !== null;
97
+ const data = config.data;
98
+ if (data === void 0 || data === null) return false;
99
+ return isJsonApiSerializableBody(data);
100
+ }
101
+ function isJsonApiSerializableBody(data) {
102
+ if (typeof data !== "object") return false;
103
+ if (Array.isArray(data)) return true;
104
+ if (typeof FormData !== "undefined" && data instanceof FormData) return false;
105
+ if (typeof Blob !== "undefined" && data instanceof Blob) return false;
106
+ if (data instanceof ArrayBuffer) return false;
107
+ if (ArrayBuffer.isView(data)) return false;
108
+ if (typeof URLSearchParams !== "undefined" && data instanceof URLSearchParams) return false;
109
+ if (data instanceof Date) return false;
110
+ if (typeof ReadableStream !== "undefined" && data instanceof ReadableStream) return false;
111
+ const proto = Object.getPrototypeOf(data);
112
+ return proto === Object.prototype || proto === null;
98
113
  }
99
114
 
100
115
  // src/headers/auth.ts
@@ -178,6 +193,9 @@ function retryAllowed(ctx) {
178
193
  return false;
179
194
  }
180
195
  if (!isRead) {
196
+ if (ctx.retryMutationsOnServerError === true && s >= 500 && s < 600) {
197
+ return true;
198
+ }
181
199
  return false;
182
200
  }
183
201
  if (s === 408) return true;
@@ -242,7 +260,8 @@ async function dispatchWithRetry(instance, config, options) {
242
260
  method: String(config.method ?? "GET"),
243
261
  status,
244
262
  primaryErrorCode: primary,
245
- isNetworkError: false
263
+ isNetworkError: false,
264
+ retryMutationsOnServerError: options.retry?.retryMutationsOnServerError
246
265
  });
247
266
  if (allowed && attempt < max - 1) {
248
267
  const retryAfter = parseRetryAfterSeconds(flat["retry-after"]);
@@ -262,7 +281,8 @@ async function dispatchWithRetry(instance, config, options) {
262
281
  const isNet = isAxiosNetworkError(err);
263
282
  const allowed = retryAllowed({
264
283
  method: String(config.method ?? "GET"),
265
- isNetworkError: isNet
284
+ isNetworkError: isNet,
285
+ retryMutationsOnServerError: options.retry?.retryMutationsOnServerError
266
286
  });
267
287
  if (!allowed || attempt >= max - 1) {
268
288
  throw err;
@@ -757,6 +777,9 @@ function createApiClient(config) {
757
777
  async post(path, data, opts) {
758
778
  return perform("POST", resolvePath(path), { ...opts, data });
759
779
  },
780
+ async postFormData(path, data, opts) {
781
+ return perform("POST", resolvePath(path), { ...opts, data });
782
+ },
760
783
  async patch(path, data, opts) {
761
784
  return perform("PATCH", resolvePath(path), { ...opts, data });
762
785
  },
@@ -789,20 +812,42 @@ function createApiClient(config) {
789
812
  ifMatchVersion: version
790
813
  });
791
814
  },
792
- safeGet: (path, opts) => safe(() => client.get(path, opts)),
793
- safePost: (path, data, opts) => safe(() => client.post(path, data, opts)),
794
- safePatch: (path, data, opts) => safe(() => client.patch(path, data, opts)),
795
- safePut: (path, data, opts) => safe(() => client.put(path, data, opts)),
796
- safeDelete: (path, opts) => safe(() => client.delete(path, opts)),
797
- safeHead: (path, opts) => safe(() => client.head(path, opts)),
798
- safeRequest: (ax, opts) => safe(() => client.request(ax, opts)),
799
- safeGetByUrl: (url, opts) => safe(() => client.getByUrl(url, opts)),
800
- safePatchWithVersion: (path, data, version, opts) => safe(() => client.patchWithVersion(path, data, version, opts))
815
+ safeGet: (path, opts) => safe(() => perform("GET", resolvePath(path), opts ?? {})),
816
+ safePost: (path, data, opts) => safe(() => perform("POST", resolvePath(path), { ...opts, data })),
817
+ safePatch: (path, data, opts) => safe(() => perform("PATCH", resolvePath(path), { ...opts, data })),
818
+ safePut: (path, data, opts) => safe(() => perform("PUT", resolvePath(path), { ...opts, data })),
819
+ safeDelete: (path, opts) => safe(() => perform("DELETE", resolvePath(path), opts ?? {})),
820
+ safeHead: (path, opts) => safe(() => perform("HEAD", resolvePath(path), opts ?? {})),
821
+ safeRequest: (ax, opts) => safe(() => {
822
+ const method = (ax.method ?? "GET").toUpperCase();
823
+ const rawUrl = ax.url ?? "/";
824
+ const u = typeof rawUrl === "string" && /^https?:\/\//i.test(rawUrl) ? rawUrl : resolvePath(rawUrl);
825
+ return perform(method, u, {
826
+ ...opts,
827
+ data: ax.data,
828
+ idempotencyKey: opts?.idempotencyKey ?? readHeader(ax, "Idempotency-Key")
829
+ });
830
+ }),
831
+ safeGetByUrl: (fullUrl, opts) => safe(() => perform("GET", fullUrl, opts ?? {})),
832
+ safePatchWithVersion: (path, data, version, opts) => safe(
833
+ () => perform("PATCH", resolvePath(path), {
834
+ ...opts,
835
+ data,
836
+ ifMatchVersion: version
837
+ })
838
+ )
801
839
  };
802
840
  return client;
803
841
  }
804
842
 
805
843
  // src/guards.ts
844
+ function hasErrorCode(error, code) {
845
+ if (!(error instanceof ApiClientError)) return false;
846
+ return error.errors.some((e) => e.code === code);
847
+ }
848
+ function isApiClientErrorWithCode(error, code) {
849
+ return hasErrorCode(error, code);
850
+ }
806
851
  function isAuthenticationError(e) {
807
852
  return isApiErr(e, 401);
808
853
  }
@@ -812,6 +857,15 @@ function isForbiddenError(e) {
812
857
  function isPreconditionRequiredError(e) {
813
858
  return isApiErr(e, 428);
814
859
  }
860
+ function isIdempotencyKeyRequiredError(e) {
861
+ return isApiErrWithCode(e, 428, "IDEMPOTENCY_KEY_REQUIRED");
862
+ }
863
+ function isIfMatchRequiredError(e) {
864
+ return isApiErrWithCode(e, 428, "IF_MATCH_REQUIRED");
865
+ }
866
+ function isMfaVerificationRequiredError(e) {
867
+ return isApiErrWithCode(e, 428, "MFA_VERIFICATION_REQUIRED");
868
+ }
815
869
  function isPreconditionFailedError(e) {
816
870
  return isApiErr(e, 412);
817
871
  }
@@ -824,18 +878,22 @@ function isConflictError(e) {
824
878
  function isPayloadTooLargeError(e) {
825
879
  return isApiErr(e, 413);
826
880
  }
827
- function isRetryablePerPolicy(e) {
881
+ function isRetryablePerPolicy(e, policy) {
828
882
  if (!(e instanceof ApiClientError)) return false;
829
883
  return retryAllowed({
830
884
  method: e.requestMethod ?? "GET",
831
885
  status: e.status,
832
886
  primaryErrorCode: e.primaryCode,
833
- isNetworkError: false
887
+ isNetworkError: false,
888
+ retryMutationsOnServerError: policy?.retryMutationsOnServerError
834
889
  });
835
890
  }
836
891
  function isApiErr(e, status) {
837
892
  return e instanceof ApiClientError && e.status === status;
838
893
  }
894
+ function isApiErrWithCode(e, status, code) {
895
+ return isApiErr(e, status) && hasErrorCode(e, code);
896
+ }
839
897
 
840
898
  // src/parse/pagination.ts
841
899
  function parsePaginationKind(meta, links) {
@@ -1047,11 +1105,16 @@ export {
1047
1105
  getHeader,
1048
1106
  getNextPageUrl,
1049
1107
  groupValidationErrorsByPointer,
1108
+ hasErrorCode,
1050
1109
  indexIncluded,
1051
1110
  isApiClientError,
1111
+ isApiClientErrorWithCode,
1052
1112
  isAuthenticationError,
1053
1113
  isConflictError,
1054
1114
  isForbiddenError,
1115
+ isIdempotencyKeyRequiredError,
1116
+ isIfMatchRequiredError,
1117
+ isMfaVerificationRequiredError,
1055
1118
  isMutationMethod,
1056
1119
  isPayloadTooLargeError,
1057
1120
  isPreconditionFailedError,