@ikatec/digisac-api-sdk 3.1.0 → 4.0.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/README.md CHANGED
@@ -165,23 +165,58 @@ const result = await people.getMany({
165
165
 
166
166
  ## Error Handling
167
167
 
168
- Failed requests throw an `ApiError` with structured information:
168
+ Every error thrown by the SDK extends `DigisacError`:
169
+
170
+ ```
171
+ DigisacError
172
+ ├── ApiConnectionError no HTTP response (network failure, DNS, connection refused)
173
+ └── ApiError non-2xx response; plain ApiError for unlisted statuses
174
+ ├── BadRequestError 400
175
+ │ └── ValidationError 400 `ValidationError`: field validation failed
176
+ ├── AuthenticationError 401
177
+ ├── PaymentRequiredError 402: out of credits, plan limits
178
+ ├── PermissionDeniedError 403
179
+ ├── NotFoundError 404
180
+ ├── ConflictError 409
181
+ ├── UnprocessableEntityError 422
182
+ ├── RateLimitError 429, with `retryAfter`
183
+ └── InternalServerError 5xx
184
+ ```
185
+
186
+ Errors are classified by HTTP status, plus `ValidationError` for field validation failures. The backend error name is always available as the `errorClass` string.
169
187
 
170
188
  ```typescript
171
- import { ApiError } from '@ikatec/digisac-api-sdk'
189
+ import {
190
+ ApiConnectionError,
191
+ ApiError,
192
+ NotFoundError,
193
+ ValidationError,
194
+ } from '@ikatec/digisac-api-sdk'
172
195
 
173
196
  try {
174
197
  await tickets.create(payload)
175
198
  } catch (error) {
176
- if (error instanceof ApiError) {
177
- console.log(error.message) // Error message from API
178
- console.log(error.status) // HTTP status code
179
- console.log(error.errorClass) // Error class identifier
180
- console.log(error.validationErrors) // Field-level validation errors
199
+ if (error instanceof ValidationError) {
200
+ for (const { field, message, code } of error.validationErrors) {
201
+ console.log(field, message, code) // 'name', 'Required.', 'required'
202
+ }
203
+ } else if (error instanceof NotFoundError) {
204
+ // ...
205
+ } else if (error instanceof ApiError) {
206
+ console.log(error.status) // HTTP status code
207
+ console.log(error.errorClass) // backend error name, e.g. 'HttpError', 'PaymentRequired'
208
+ console.log(error.message) // backend message
209
+ console.log(error.body) // full parsed error body (or raw text)
210
+ console.log(error.headers) // response headers
211
+ } else if (error instanceof ApiConnectionError) {
212
+ console.log(error.cause) // the original fetch error
181
213
  }
182
214
  }
183
215
  ```
184
216
 
217
+ - `validationErrors` is a flat list of `FieldError` (`{ location, field, path, message, code }`), one entry per failed rule, e.g. `{ location: 'body', field: 'name', path: 'body.name', message: 'Required.', code: 'required' }`. On `ValidationError` it is always an array; on other errors it is `null` when the response has no field errors.
218
+ - `RateLimitError.retryAfter` is the number of seconds to wait, read from `Retry-After` or the backend's `X-RateLimit-Reset`, or `null` when the response does not say.
219
+
185
220
  ## Incoming Webhooks
186
221
 
187
222
  The SDK ships a fully-typed `WebhookPayload<E>` envelope and a `WebhookEventPayloadMap` that narrows `data` to the correct resource type for every supported event.
@@ -1,43 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_core_errors = require('./errors.cjs');
2
3
 
3
4
  //#region src/core/BaseApiClient.ts
4
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5
- /**
6
- * Flattens the Digisac `errors` object into `ValidationError[]` — one entry per message, with
7
- * `path` = `<location>.<field>` (e.g. `body.name`) and `code` = the rule type (e.g. `required`).
8
- * Returns `null` when the body carries no field errors.
9
- */
10
- function parseValidationErrors(errors) {
11
- if (!isRecord(errors)) return null;
12
- const parsed = [];
13
- for (const [location, fields] of Object.entries(errors)) {
14
- if (!isRecord(fields)) continue;
15
- for (const [field, detail] of Object.entries(fields)) {
16
- const messages = Array.isArray(detail?.messages) ? detail.messages.map(String) : [];
17
- const types = Array.isArray(detail?.types) ? detail.types.map(String) : [];
18
- messages.forEach((message, index) => {
19
- parsed.push({
20
- path: `${location}.${field}`,
21
- message,
22
- code: types[index] ?? types[0] ?? ""
23
- });
24
- });
25
- }
26
- }
27
- return parsed.length ? parsed : null;
28
- }
29
- var ApiError = class extends Error {
30
- errorClass;
31
- status;
32
- validationErrors;
33
- constructor(message, errorClass, status, validationErrors = null) {
34
- super(message);
35
- this.name = "ApiError";
36
- this.errorClass = errorClass;
37
- this.status = status;
38
- this.validationErrors = validationErrors;
39
- }
40
- };
41
5
  var BaseApiClient = class {
42
6
  baseUrl;
43
7
  accessToken = null;
@@ -51,7 +15,7 @@ var BaseApiClient = class {
51
15
  }
52
16
  async request(method, endpoint, body, headers) {
53
17
  const url = `${this.baseUrl}/${endpoint.replace(/^\//, "")}`;
54
- const response = await fetch(url, {
18
+ const init = {
55
19
  method,
56
20
  headers: {
57
21
  "Content-Type": "application/json",
@@ -59,26 +23,17 @@ var BaseApiClient = class {
59
23
  ...headers
60
24
  },
61
25
  ...body !== void 0 && { body: JSON.stringify(body) }
62
- });
26
+ };
27
+ let response;
28
+ try {
29
+ response = await fetch(url, init);
30
+ } catch (error) {
31
+ throw new require_core_errors.ApiConnectionError(`Request to ${method} ${url} failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
32
+ }
63
33
  return this.handleResponse(response);
64
34
  }
65
35
  async handleResponse(response) {
66
- if (!response.ok) {
67
- const isJson = response.headers.get("Content-Type")?.includes("application/json") ?? false;
68
- let errorMessage = `HTTP Error ${response.status}: ${response.statusText}`;
69
- let errorClass;
70
- let validationErrors = null;
71
- if (isJson) try {
72
- const errorText = await response.text();
73
- if (errorText) {
74
- const errorData = JSON.parse(errorText);
75
- errorMessage = errorData.message || errorData.error || errorMessage;
76
- errorClass = errorData.error;
77
- validationErrors = errorData.validationErrors ?? parseValidationErrors(errorData.errors);
78
- }
79
- } catch {}
80
- throw new ApiError(errorMessage, errorClass, response.status, validationErrors);
81
- }
36
+ if (!response.ok) throw require_core_errors.ApiError.fromResponse(response.status, response.statusText, response.headers, await this.readErrorBody(response));
82
37
  if (response.status === 204) return;
83
38
  const text = await response.text();
84
39
  if (!text) return;
@@ -89,6 +44,21 @@ var BaseApiClient = class {
89
44
  return text;
90
45
  }
91
46
  }
47
+ /** Reads an error body: parsed JSON when possible, the raw text otherwise, `undefined` when empty. */
48
+ async readErrorBody(response) {
49
+ let text;
50
+ try {
51
+ text = await response.text();
52
+ } catch {
53
+ return;
54
+ }
55
+ if (!text) return;
56
+ try {
57
+ return JSON.parse(text);
58
+ } catch {
59
+ return text;
60
+ }
61
+ }
92
62
  get(endpoint, headers) {
93
63
  return this.request("GET", endpoint, void 0, headers);
94
64
  }
@@ -107,6 +77,5 @@ var BaseApiClient = class {
107
77
  };
108
78
 
109
79
  //#endregion
110
- exports.ApiError = ApiError;
111
80
  exports.BaseApiClient = BaseApiClient;
112
81
  //# sourceMappingURL=BaseApiClient.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"BaseApiClient.cjs","names":[],"sources":["../../src/core/BaseApiClient.ts"],"sourcesContent":["import type { ApiClient, HttpMethod } from './ApiClient'\n\nexport interface ValidationError {\n path: string\n message: string\n code: string\n}\n\n/**\n * Digisac field errors: `{ errors: { <location>: { <field>: { messages: string[], types: string[] } } } }`,\n * where location is `body`, `params`, `query`, `headers` or `cookies` (backend `validate` middleware).\n */\ntype DigisacFieldErrors = Record<string, Record<string, { messages?: unknown; types?: unknown }>>\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Flattens the Digisac `errors` object into `ValidationError[]` — one entry per message, with\n * `path` = `<location>.<field>` (e.g. `body.name`) and `code` = the rule type (e.g. `required`).\n * Returns `null` when the body carries no field errors.\n */\nfunction parseValidationErrors(errors: unknown): ValidationError[] | null {\n if (!isRecord(errors)) {\n return null\n }\n\n const parsed: ValidationError[] = []\n for (const [location, fields] of Object.entries(errors as DigisacFieldErrors)) {\n if (!isRecord(fields)) {\n continue\n }\n for (const [field, detail] of Object.entries(fields)) {\n const messages = Array.isArray(detail?.messages) ? detail.messages.map(String) : []\n const types = Array.isArray(detail?.types) ? detail.types.map(String) : []\n messages.forEach((message, index) => {\n parsed.push({ path: `${location}.${field}`, message, code: types[index] ?? types[0] ?? '' })\n })\n }\n }\n\n return parsed.length ? parsed : null\n}\n\nexport class ApiError extends Error {\n public readonly errorClass?: string | undefined\n public readonly status?: number | undefined\n public readonly validationErrors: ValidationError[] | null\n\n constructor(\n message: string,\n errorClass?: string,\n status?: number,\n validationErrors: ValidationError[] | null = null,\n ) {\n super(message)\n this.name = 'ApiError'\n this.errorClass = errorClass\n this.status = status\n this.validationErrors = validationErrors\n }\n}\n\nexport class BaseApiClient implements ApiClient {\n protected readonly baseUrl: string\n protected accessToken: string | null = null\n\n constructor(baseUrl: string, accessToken?: string) {\n this.baseUrl = baseUrl.replace(/\\/$/, '')\n this.accessToken = accessToken || null\n }\n\n setAccessToken(accessToken: string): this {\n this.accessToken = accessToken\n return this\n }\n\n async request<TResponse = unknown, TBody = unknown>(\n method: HttpMethod,\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n const url = `${this.baseUrl}/${endpoint.replace(/^\\//, '')}`\n\n const response = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...(this.accessToken && { Authorization: `Bearer ${this.accessToken}` }),\n ...headers,\n },\n ...(body !== undefined && { body: JSON.stringify(body) }),\n })\n\n return this.handleResponse<TResponse>(response)\n }\n\n protected async handleResponse<TResponse>(response: Response): Promise<TResponse> {\n if (!response.ok) {\n const contentType = response.headers.get('Content-Type')\n const isJson = contentType?.includes('application/json') ?? false\n\n let errorMessage = `HTTP Error ${response.status}: ${response.statusText}`\n let errorClass: string | undefined\n let validationErrors: ValidationError[] | null = null\n\n if (isJson) {\n try {\n const errorText = await response.text()\n if (errorText) {\n const errorData = JSON.parse(errorText)\n errorMessage = errorData.message || errorData.error || errorMessage\n errorClass = errorData.error\n validationErrors = errorData.validationErrors ?? parseValidationErrors(errorData.errors)\n }\n } catch {\n // If JSON parsing fails, use the default error message\n }\n }\n\n throw new ApiError(errorMessage, errorClass, response.status, validationErrors)\n }\n\n // 204 No Content never has a body; short-circuit before reading the stream.\n if (response.status === 204) {\n return undefined as unknown as TResponse\n }\n\n // Read body as text once and treat an empty body as \"no content\" regardless of\n // Content-Type. Servers sometimes return 200 with Content-Type: application/json\n // and an empty body (e.g. POST endpoints whose framework keeps the default JSON\n // content-type even when nothing is written). `response.json()` would throw\n // \"Unexpected end of input\" in that case.\n const text = await response.text()\n if (!text) {\n return undefined as unknown as TResponse\n }\n\n const contentType = response.headers.get('Content-Type')\n const isJson = contentType?.includes('application/json') ?? false\n\n if (isJson) {\n return JSON.parse(text) as TResponse\n }\n\n try {\n return JSON.parse(text) as TResponse\n } catch {\n return text as unknown as TResponse\n }\n }\n\n get<TResponse = unknown>(endpoint: string, headers?: Record<string, string>): Promise<TResponse> {\n return this.request<TResponse>('GET', endpoint, undefined, headers)\n }\n\n post<TResponse = unknown, TBody = unknown>(\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse, TBody>('POST', endpoint, body, headers)\n }\n\n put<TResponse = unknown, TBody = unknown>(\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse, TBody>('PUT', endpoint, body, headers)\n }\n\n patch<TResponse = unknown, TBody = unknown>(\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse, TBody>('PATCH', endpoint, body, headers)\n }\n\n delete<TResponse = unknown>(\n endpoint: string,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse>('DELETE', endpoint, undefined, headers)\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"BaseApiClient.cjs","names":["ApiConnectionError","ApiError"],"sources":["../../src/core/BaseApiClient.ts"],"sourcesContent":["import type { ApiClient, HttpMethod } from './ApiClient'\nimport { ApiConnectionError, ApiError } from './errors'\n\nexport class BaseApiClient implements ApiClient {\n protected readonly baseUrl: string\n protected accessToken: string | null = null\n\n constructor(baseUrl: string, accessToken?: string) {\n this.baseUrl = baseUrl.replace(/\\/$/, '')\n this.accessToken = accessToken || null\n }\n\n setAccessToken(accessToken: string): this {\n this.accessToken = accessToken\n return this\n }\n\n async request<TResponse = unknown, TBody = unknown>(\n method: HttpMethod,\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n const url = `${this.baseUrl}/${endpoint.replace(/^\\//, '')}`\n const init: RequestInit = {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...(this.accessToken && { Authorization: `Bearer ${this.accessToken}` }),\n ...headers,\n },\n ...(body !== undefined && { body: JSON.stringify(body) }),\n }\n\n let response: Response\n try {\n response = await fetch(url, init)\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n throw new ApiConnectionError(`Request to ${method} ${url} failed: ${reason}`, {\n cause: error,\n })\n }\n\n return this.handleResponse<TResponse>(response)\n }\n\n protected async handleResponse<TResponse>(response: Response): Promise<TResponse> {\n if (!response.ok) {\n throw ApiError.fromResponse(\n response.status,\n response.statusText,\n response.headers,\n await this.readErrorBody(response),\n )\n }\n\n // 204 No Content never has a body; short-circuit before reading the stream.\n if (response.status === 204) {\n return undefined as unknown as TResponse\n }\n\n // Read body as text once and treat an empty body as \"no content\" regardless of\n // Content-Type. Servers sometimes return 200 with Content-Type: application/json\n // and an empty body (e.g. POST endpoints whose framework keeps the default JSON\n // content-type even when nothing is written). `response.json()` would throw\n // \"Unexpected end of input\" in that case.\n const text = await response.text()\n if (!text) {\n return undefined as unknown as TResponse\n }\n\n const contentType = response.headers.get('Content-Type')\n const isJson = contentType?.includes('application/json') ?? false\n\n if (isJson) {\n return JSON.parse(text) as TResponse\n }\n\n try {\n return JSON.parse(text) as TResponse\n } catch {\n return text as unknown as TResponse\n }\n }\n\n /** Reads an error body: parsed JSON when possible, the raw text otherwise, `undefined` when empty. */\n protected async readErrorBody(response: Response): Promise<unknown> {\n let text: string\n try {\n text = await response.text()\n } catch {\n return undefined\n }\n if (!text) {\n return undefined\n }\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n }\n\n get<TResponse = unknown>(endpoint: string, headers?: Record<string, string>): Promise<TResponse> {\n return this.request<TResponse>('GET', endpoint, undefined, headers)\n }\n\n post<TResponse = unknown, TBody = unknown>(\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse, TBody>('POST', endpoint, body, headers)\n }\n\n put<TResponse = unknown, TBody = unknown>(\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse, TBody>('PUT', endpoint, body, headers)\n }\n\n patch<TResponse = unknown, TBody = unknown>(\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse, TBody>('PATCH', endpoint, body, headers)\n }\n\n delete<TResponse = unknown>(\n endpoint: string,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse>('DELETE', endpoint, undefined, headers)\n }\n}\n"],"mappings":";;;;AAGA,IAAa,gBAAb,MAAgD;CAC9C,AAAmB;CACnB,AAAU,cAA6B;CAEvC,YAAY,SAAiB,aAAsB;AACjD,OAAK,UAAU,QAAQ,QAAQ,OAAO,GAAG;AACzC,OAAK,cAAc,eAAe;;CAGpC,eAAe,aAA2B;AACxC,OAAK,cAAc;AACnB,SAAO;;CAGT,MAAM,QACJ,QACA,UACA,MACA,SACoB;EACpB,MAAM,MAAM,GAAG,KAAK,QAAQ,GAAG,SAAS,QAAQ,OAAO,GAAG;EAC1D,MAAM,OAAoB;GACxB;GACA,SAAS;IACP,gBAAgB;IAChB,GAAI,KAAK,eAAe,EAAE,eAAe,UAAU,KAAK,eAAe;IACvE,GAAG;IACJ;GACD,GAAI,SAAS,UAAa,EAAE,MAAM,KAAK,UAAU,KAAK,EAAE;GACzD;EAED,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,MAAM,KAAK,KAAK;WAC1B,OAAO;AAEd,SAAM,IAAIA,uCAAmB,cAAc,OAAO,GAAG,IAAI,WAD1C,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,IACS,EAC5E,OAAO,OACR,CAAC;;AAGJ,SAAO,KAAK,eAA0B,SAAS;;CAGjD,MAAgB,eAA0B,UAAwC;AAChF,MAAI,CAAC,SAAS,GACZ,OAAMC,6BAAS,aACb,SAAS,QACT,SAAS,YACT,SAAS,SACT,MAAM,KAAK,cAAc,SAAS,CACnC;AAIH,MAAI,SAAS,WAAW,IACtB;EAQF,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,MAAI,CAAC,KACH;AAMF,MAHoB,SAAS,QAAQ,IAAI,eAAe,EAC5B,SAAS,mBAAmB,IAAI,MAG1D,QAAO,KAAK,MAAM,KAAK;AAGzB,MAAI;AACF,UAAO,KAAK,MAAM,KAAK;UACjB;AACN,UAAO;;;;CAKX,MAAgB,cAAc,UAAsC;EAClE,IAAI;AACJ,MAAI;AACF,UAAO,MAAM,SAAS,MAAM;UACtB;AACN;;AAEF,MAAI,CAAC,KACH;AAEF,MAAI;AACF,UAAO,KAAK,MAAM,KAAK;UACjB;AACN,UAAO;;;CAIX,IAAyB,UAAkB,SAAsD;AAC/F,SAAO,KAAK,QAAmB,OAAO,UAAU,QAAW,QAAQ;;CAGrE,KACE,UACA,MACA,SACoB;AACpB,SAAO,KAAK,QAA0B,QAAQ,UAAU,MAAM,QAAQ;;CAGxE,IACE,UACA,MACA,SACoB;AACpB,SAAO,KAAK,QAA0B,OAAO,UAAU,MAAM,QAAQ;;CAGvE,MACE,UACA,MACA,SACoB;AACpB,SAAO,KAAK,QAA0B,SAAS,UAAU,MAAM,QAAQ;;CAGzE,OACE,UACA,SACoB;AACpB,SAAO,KAAK,QAAmB,UAAU,UAAU,QAAW,QAAQ"}
@@ -1,15 +1,4 @@
1
1
  import type { ApiClient, HttpMethod } from './ApiClient';
2
- export interface ValidationError {
3
- path: string;
4
- message: string;
5
- code: string;
6
- }
7
- export declare class ApiError extends Error {
8
- readonly errorClass?: string | undefined;
9
- readonly status?: number | undefined;
10
- readonly validationErrors: ValidationError[] | null;
11
- constructor(message: string, errorClass?: string, status?: number, validationErrors?: ValidationError[] | null);
12
- }
13
2
  export declare class BaseApiClient implements ApiClient {
14
3
  protected readonly baseUrl: string;
15
4
  protected accessToken: string | null;
@@ -17,6 +6,8 @@ export declare class BaseApiClient implements ApiClient {
17
6
  setAccessToken(accessToken: string): this;
18
7
  request<TResponse = unknown, TBody = unknown>(method: HttpMethod, endpoint: string, body?: TBody, headers?: Record<string, string>): Promise<TResponse>;
19
8
  protected handleResponse<TResponse>(response: Response): Promise<TResponse>;
9
+ /** Reads an error body: parsed JSON when possible, the raw text otherwise, `undefined` when empty. */
10
+ protected readErrorBody(response: Response): Promise<unknown>;
20
11
  get<TResponse = unknown>(endpoint: string, headers?: Record<string, string>): Promise<TResponse>;
21
12
  post<TResponse = unknown, TBody = unknown>(endpoint: string, body?: TBody, headers?: Record<string, string>): Promise<TResponse>;
22
13
  put<TResponse = unknown, TBody = unknown>(endpoint: string, body?: TBody, headers?: Record<string, string>): Promise<TResponse>;
@@ -1 +1 @@
1
- {"version":3,"file":"BaseApiClient.d.ts","sourceRoot":"","sources":["../../src/core/BaseApiClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb;AAsCD,qBAAa,QAAS,SAAQ,KAAK;IACjC,SAAgB,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC/C,SAAgB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3C,SAAgB,gBAAgB,EAAE,eAAe,EAAE,GAAG,IAAI,CAAA;gBAGxD,OAAO,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,MAAM,EACf,gBAAgB,GAAE,eAAe,EAAE,GAAG,IAAW;CAQpD;AAED,qBAAa,aAAc,YAAW,SAAS;IAC7C,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IAClC,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAO;gBAE/B,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM;IAKjD,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAKnC,OAAO,CAAC,SAAS,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EAChD,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,SAAS,CAAC;cAgBL,cAAc,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IAuDjF,GAAG,CAAC,SAAS,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC;IAIhG,IAAI,CAAC,SAAS,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EACvC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,SAAS,CAAC;IAIrB,GAAG,CAAC,SAAS,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EACtC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,SAAS,CAAC;IAIrB,KAAK,CAAC,SAAS,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EACxC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,SAAS,CAAC;IAIrB,MAAM,CAAC,SAAS,GAAG,OAAO,EACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,SAAS,CAAC;CAGtB"}
1
+ {"version":3,"file":"BaseApiClient.d.ts","sourceRoot":"","sources":["../../src/core/BaseApiClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGxD,qBAAa,aAAc,YAAW,SAAS;IAC7C,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IAClC,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAO;gBAE/B,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM;IAKjD,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAKnC,OAAO,CAAC,SAAS,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EAChD,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,SAAS,CAAC;cAyBL,cAAc,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IAuCjF,sGAAsG;cACtF,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBnE,GAAG,CAAC,SAAS,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC;IAIhG,IAAI,CAAC,SAAS,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EACvC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,SAAS,CAAC;IAIrB,GAAG,CAAC,SAAS,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EACtC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,SAAS,CAAC;IAIrB,KAAK,CAAC,SAAS,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EACxC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,SAAS,CAAC;IAIrB,MAAM,CAAC,SAAS,GAAG,OAAO,EACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,SAAS,CAAC;CAGtB"}
@@ -1,41 +1,6 @@
1
+ import { ApiConnectionError, ApiError } from "./errors.mjs";
2
+
1
3
  //#region src/core/BaseApiClient.ts
2
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3
- /**
4
- * Flattens the Digisac `errors` object into `ValidationError[]` — one entry per message, with
5
- * `path` = `<location>.<field>` (e.g. `body.name`) and `code` = the rule type (e.g. `required`).
6
- * Returns `null` when the body carries no field errors.
7
- */
8
- function parseValidationErrors(errors) {
9
- if (!isRecord(errors)) return null;
10
- const parsed = [];
11
- for (const [location, fields] of Object.entries(errors)) {
12
- if (!isRecord(fields)) continue;
13
- for (const [field, detail] of Object.entries(fields)) {
14
- const messages = Array.isArray(detail?.messages) ? detail.messages.map(String) : [];
15
- const types = Array.isArray(detail?.types) ? detail.types.map(String) : [];
16
- messages.forEach((message, index) => {
17
- parsed.push({
18
- path: `${location}.${field}`,
19
- message,
20
- code: types[index] ?? types[0] ?? ""
21
- });
22
- });
23
- }
24
- }
25
- return parsed.length ? parsed : null;
26
- }
27
- var ApiError = class extends Error {
28
- errorClass;
29
- status;
30
- validationErrors;
31
- constructor(message, errorClass, status, validationErrors = null) {
32
- super(message);
33
- this.name = "ApiError";
34
- this.errorClass = errorClass;
35
- this.status = status;
36
- this.validationErrors = validationErrors;
37
- }
38
- };
39
4
  var BaseApiClient = class {
40
5
  baseUrl;
41
6
  accessToken = null;
@@ -49,7 +14,7 @@ var BaseApiClient = class {
49
14
  }
50
15
  async request(method, endpoint, body, headers) {
51
16
  const url = `${this.baseUrl}/${endpoint.replace(/^\//, "")}`;
52
- const response = await fetch(url, {
17
+ const init = {
53
18
  method,
54
19
  headers: {
55
20
  "Content-Type": "application/json",
@@ -57,26 +22,17 @@ var BaseApiClient = class {
57
22
  ...headers
58
23
  },
59
24
  ...body !== void 0 && { body: JSON.stringify(body) }
60
- });
25
+ };
26
+ let response;
27
+ try {
28
+ response = await fetch(url, init);
29
+ } catch (error) {
30
+ throw new ApiConnectionError(`Request to ${method} ${url} failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
31
+ }
61
32
  return this.handleResponse(response);
62
33
  }
63
34
  async handleResponse(response) {
64
- if (!response.ok) {
65
- const isJson = response.headers.get("Content-Type")?.includes("application/json") ?? false;
66
- let errorMessage = `HTTP Error ${response.status}: ${response.statusText}`;
67
- let errorClass;
68
- let validationErrors = null;
69
- if (isJson) try {
70
- const errorText = await response.text();
71
- if (errorText) {
72
- const errorData = JSON.parse(errorText);
73
- errorMessage = errorData.message || errorData.error || errorMessage;
74
- errorClass = errorData.error;
75
- validationErrors = errorData.validationErrors ?? parseValidationErrors(errorData.errors);
76
- }
77
- } catch {}
78
- throw new ApiError(errorMessage, errorClass, response.status, validationErrors);
79
- }
35
+ if (!response.ok) throw ApiError.fromResponse(response.status, response.statusText, response.headers, await this.readErrorBody(response));
80
36
  if (response.status === 204) return;
81
37
  const text = await response.text();
82
38
  if (!text) return;
@@ -87,6 +43,21 @@ var BaseApiClient = class {
87
43
  return text;
88
44
  }
89
45
  }
46
+ /** Reads an error body: parsed JSON when possible, the raw text otherwise, `undefined` when empty. */
47
+ async readErrorBody(response) {
48
+ let text;
49
+ try {
50
+ text = await response.text();
51
+ } catch {
52
+ return;
53
+ }
54
+ if (!text) return;
55
+ try {
56
+ return JSON.parse(text);
57
+ } catch {
58
+ return text;
59
+ }
60
+ }
90
61
  get(endpoint, headers) {
91
62
  return this.request("GET", endpoint, void 0, headers);
92
63
  }
@@ -105,5 +76,5 @@ var BaseApiClient = class {
105
76
  };
106
77
 
107
78
  //#endregion
108
- export { ApiError, BaseApiClient };
79
+ export { BaseApiClient };
109
80
  //# sourceMappingURL=BaseApiClient.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"BaseApiClient.mjs","names":[],"sources":["../../src/core/BaseApiClient.ts"],"sourcesContent":["import type { ApiClient, HttpMethod } from './ApiClient'\n\nexport interface ValidationError {\n path: string\n message: string\n code: string\n}\n\n/**\n * Digisac field errors: `{ errors: { <location>: { <field>: { messages: string[], types: string[] } } } }`,\n * where location is `body`, `params`, `query`, `headers` or `cookies` (backend `validate` middleware).\n */\ntype DigisacFieldErrors = Record<string, Record<string, { messages?: unknown; types?: unknown }>>\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Flattens the Digisac `errors` object into `ValidationError[]` — one entry per message, with\n * `path` = `<location>.<field>` (e.g. `body.name`) and `code` = the rule type (e.g. `required`).\n * Returns `null` when the body carries no field errors.\n */\nfunction parseValidationErrors(errors: unknown): ValidationError[] | null {\n if (!isRecord(errors)) {\n return null\n }\n\n const parsed: ValidationError[] = []\n for (const [location, fields] of Object.entries(errors as DigisacFieldErrors)) {\n if (!isRecord(fields)) {\n continue\n }\n for (const [field, detail] of Object.entries(fields)) {\n const messages = Array.isArray(detail?.messages) ? detail.messages.map(String) : []\n const types = Array.isArray(detail?.types) ? detail.types.map(String) : []\n messages.forEach((message, index) => {\n parsed.push({ path: `${location}.${field}`, message, code: types[index] ?? types[0] ?? '' })\n })\n }\n }\n\n return parsed.length ? parsed : null\n}\n\nexport class ApiError extends Error {\n public readonly errorClass?: string | undefined\n public readonly status?: number | undefined\n public readonly validationErrors: ValidationError[] | null\n\n constructor(\n message: string,\n errorClass?: string,\n status?: number,\n validationErrors: ValidationError[] | null = null,\n ) {\n super(message)\n this.name = 'ApiError'\n this.errorClass = errorClass\n this.status = status\n this.validationErrors = validationErrors\n }\n}\n\nexport class BaseApiClient implements ApiClient {\n protected readonly baseUrl: string\n protected accessToken: string | null = null\n\n constructor(baseUrl: string, accessToken?: string) {\n this.baseUrl = baseUrl.replace(/\\/$/, '')\n this.accessToken = accessToken || null\n }\n\n setAccessToken(accessToken: string): this {\n this.accessToken = accessToken\n return this\n }\n\n async request<TResponse = unknown, TBody = unknown>(\n method: HttpMethod,\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n const url = `${this.baseUrl}/${endpoint.replace(/^\\//, '')}`\n\n const response = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...(this.accessToken && { Authorization: `Bearer ${this.accessToken}` }),\n ...headers,\n },\n ...(body !== undefined && { body: JSON.stringify(body) }),\n })\n\n return this.handleResponse<TResponse>(response)\n }\n\n protected async handleResponse<TResponse>(response: Response): Promise<TResponse> {\n if (!response.ok) {\n const contentType = response.headers.get('Content-Type')\n const isJson = contentType?.includes('application/json') ?? false\n\n let errorMessage = `HTTP Error ${response.status}: ${response.statusText}`\n let errorClass: string | undefined\n let validationErrors: ValidationError[] | null = null\n\n if (isJson) {\n try {\n const errorText = await response.text()\n if (errorText) {\n const errorData = JSON.parse(errorText)\n errorMessage = errorData.message || errorData.error || errorMessage\n errorClass = errorData.error\n validationErrors = errorData.validationErrors ?? parseValidationErrors(errorData.errors)\n }\n } catch {\n // If JSON parsing fails, use the default error message\n }\n }\n\n throw new ApiError(errorMessage, errorClass, response.status, validationErrors)\n }\n\n // 204 No Content never has a body; short-circuit before reading the stream.\n if (response.status === 204) {\n return undefined as unknown as TResponse\n }\n\n // Read body as text once and treat an empty body as \"no content\" regardless of\n // Content-Type. Servers sometimes return 200 with Content-Type: application/json\n // and an empty body (e.g. POST endpoints whose framework keeps the default JSON\n // content-type even when nothing is written). `response.json()` would throw\n // \"Unexpected end of input\" in that case.\n const text = await response.text()\n if (!text) {\n return undefined as unknown as TResponse\n }\n\n const contentType = response.headers.get('Content-Type')\n const isJson = contentType?.includes('application/json') ?? false\n\n if (isJson) {\n return JSON.parse(text) as TResponse\n }\n\n try {\n return JSON.parse(text) as TResponse\n } catch {\n return text as unknown as TResponse\n }\n }\n\n get<TResponse = unknown>(endpoint: string, headers?: Record<string, string>): Promise<TResponse> {\n return this.request<TResponse>('GET', endpoint, undefined, headers)\n }\n\n post<TResponse = unknown, TBody = unknown>(\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse, TBody>('POST', endpoint, body, headers)\n }\n\n put<TResponse = unknown, TBody = unknown>(\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse, TBody>('PUT', endpoint, body, headers)\n }\n\n patch<TResponse = unknown, TBody = unknown>(\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse, TBody>('PATCH', endpoint, body, headers)\n }\n\n delete<TResponse = unknown>(\n endpoint: string,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse>('DELETE', endpoint, undefined, headers)\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"BaseApiClient.mjs","names":[],"sources":["../../src/core/BaseApiClient.ts"],"sourcesContent":["import type { ApiClient, HttpMethod } from './ApiClient'\nimport { ApiConnectionError, ApiError } from './errors'\n\nexport class BaseApiClient implements ApiClient {\n protected readonly baseUrl: string\n protected accessToken: string | null = null\n\n constructor(baseUrl: string, accessToken?: string) {\n this.baseUrl = baseUrl.replace(/\\/$/, '')\n this.accessToken = accessToken || null\n }\n\n setAccessToken(accessToken: string): this {\n this.accessToken = accessToken\n return this\n }\n\n async request<TResponse = unknown, TBody = unknown>(\n method: HttpMethod,\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n const url = `${this.baseUrl}/${endpoint.replace(/^\\//, '')}`\n const init: RequestInit = {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...(this.accessToken && { Authorization: `Bearer ${this.accessToken}` }),\n ...headers,\n },\n ...(body !== undefined && { body: JSON.stringify(body) }),\n }\n\n let response: Response\n try {\n response = await fetch(url, init)\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n throw new ApiConnectionError(`Request to ${method} ${url} failed: ${reason}`, {\n cause: error,\n })\n }\n\n return this.handleResponse<TResponse>(response)\n }\n\n protected async handleResponse<TResponse>(response: Response): Promise<TResponse> {\n if (!response.ok) {\n throw ApiError.fromResponse(\n response.status,\n response.statusText,\n response.headers,\n await this.readErrorBody(response),\n )\n }\n\n // 204 No Content never has a body; short-circuit before reading the stream.\n if (response.status === 204) {\n return undefined as unknown as TResponse\n }\n\n // Read body as text once and treat an empty body as \"no content\" regardless of\n // Content-Type. Servers sometimes return 200 with Content-Type: application/json\n // and an empty body (e.g. POST endpoints whose framework keeps the default JSON\n // content-type even when nothing is written). `response.json()` would throw\n // \"Unexpected end of input\" in that case.\n const text = await response.text()\n if (!text) {\n return undefined as unknown as TResponse\n }\n\n const contentType = response.headers.get('Content-Type')\n const isJson = contentType?.includes('application/json') ?? false\n\n if (isJson) {\n return JSON.parse(text) as TResponse\n }\n\n try {\n return JSON.parse(text) as TResponse\n } catch {\n return text as unknown as TResponse\n }\n }\n\n /** Reads an error body: parsed JSON when possible, the raw text otherwise, `undefined` when empty. */\n protected async readErrorBody(response: Response): Promise<unknown> {\n let text: string\n try {\n text = await response.text()\n } catch {\n return undefined\n }\n if (!text) {\n return undefined\n }\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n }\n\n get<TResponse = unknown>(endpoint: string, headers?: Record<string, string>): Promise<TResponse> {\n return this.request<TResponse>('GET', endpoint, undefined, headers)\n }\n\n post<TResponse = unknown, TBody = unknown>(\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse, TBody>('POST', endpoint, body, headers)\n }\n\n put<TResponse = unknown, TBody = unknown>(\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse, TBody>('PUT', endpoint, body, headers)\n }\n\n patch<TResponse = unknown, TBody = unknown>(\n endpoint: string,\n body?: TBody,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse, TBody>('PATCH', endpoint, body, headers)\n }\n\n delete<TResponse = unknown>(\n endpoint: string,\n headers?: Record<string, string>,\n ): Promise<TResponse> {\n return this.request<TResponse>('DELETE', endpoint, undefined, headers)\n }\n}\n"],"mappings":";;;AAGA,IAAa,gBAAb,MAAgD;CAC9C,AAAmB;CACnB,AAAU,cAA6B;CAEvC,YAAY,SAAiB,aAAsB;AACjD,OAAK,UAAU,QAAQ,QAAQ,OAAO,GAAG;AACzC,OAAK,cAAc,eAAe;;CAGpC,eAAe,aAA2B;AACxC,OAAK,cAAc;AACnB,SAAO;;CAGT,MAAM,QACJ,QACA,UACA,MACA,SACoB;EACpB,MAAM,MAAM,GAAG,KAAK,QAAQ,GAAG,SAAS,QAAQ,OAAO,GAAG;EAC1D,MAAM,OAAoB;GACxB;GACA,SAAS;IACP,gBAAgB;IAChB,GAAI,KAAK,eAAe,EAAE,eAAe,UAAU,KAAK,eAAe;IACvE,GAAG;IACJ;GACD,GAAI,SAAS,UAAa,EAAE,MAAM,KAAK,UAAU,KAAK,EAAE;GACzD;EAED,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,MAAM,KAAK,KAAK;WAC1B,OAAO;AAEd,SAAM,IAAI,mBAAmB,cAAc,OAAO,GAAG,IAAI,WAD1C,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,IACS,EAC5E,OAAO,OACR,CAAC;;AAGJ,SAAO,KAAK,eAA0B,SAAS;;CAGjD,MAAgB,eAA0B,UAAwC;AAChF,MAAI,CAAC,SAAS,GACZ,OAAM,SAAS,aACb,SAAS,QACT,SAAS,YACT,SAAS,SACT,MAAM,KAAK,cAAc,SAAS,CACnC;AAIH,MAAI,SAAS,WAAW,IACtB;EAQF,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,MAAI,CAAC,KACH;AAMF,MAHoB,SAAS,QAAQ,IAAI,eAAe,EAC5B,SAAS,mBAAmB,IAAI,MAG1D,QAAO,KAAK,MAAM,KAAK;AAGzB,MAAI;AACF,UAAO,KAAK,MAAM,KAAK;UACjB;AACN,UAAO;;;;CAKX,MAAgB,cAAc,UAAsC;EAClE,IAAI;AACJ,MAAI;AACF,UAAO,MAAM,SAAS,MAAM;UACtB;AACN;;AAEF,MAAI,CAAC,KACH;AAEF,MAAI;AACF,UAAO,KAAK,MAAM,KAAK;UACjB;AACN,UAAO;;;CAIX,IAAyB,UAAkB,SAAsD;AAC/F,SAAO,KAAK,QAAmB,OAAO,UAAU,QAAW,QAAQ;;CAGrE,KACE,UACA,MACA,SACoB;AACpB,SAAO,KAAK,QAA0B,QAAQ,UAAU,MAAM,QAAQ;;CAGxE,IACE,UACA,MACA,SACoB;AACpB,SAAO,KAAK,QAA0B,OAAO,UAAU,MAAM,QAAQ;;CAGvE,MACE,UACA,MACA,SACoB;AACpB,SAAO,KAAK,QAA0B,SAAS,UAAU,MAAM,QAAQ;;CAGzE,OACE,UACA,SACoB;AACpB,SAAO,KAAK,QAAmB,UAAU,UAAU,QAAW,QAAQ"}
@@ -0,0 +1,192 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/core/errors.ts
4
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5
+ /**
6
+ * Flattens the backend `errors` object — `{ <location>: { <field>: { messages: string[], types: string[] } } }`
7
+ * — into one {@link FieldError} per message. Returns `null` when there are no field errors.
8
+ */
9
+ function parseFieldErrors(errors) {
10
+ if (!isRecord(errors)) return null;
11
+ const parsed = [];
12
+ for (const [location, fields] of Object.entries(errors)) {
13
+ if (!isRecord(fields)) continue;
14
+ for (const [field, detail] of Object.entries(fields)) {
15
+ if (!isRecord(detail)) continue;
16
+ const messages = Array.isArray(detail.messages) ? detail.messages.map(String) : [];
17
+ const types = Array.isArray(detail.types) ? detail.types.map(String) : [];
18
+ messages.forEach((message, index) => {
19
+ parsed.push({
20
+ location,
21
+ field,
22
+ path: `${location}.${field}`,
23
+ message,
24
+ code: types[index] ?? types[0] ?? ""
25
+ });
26
+ });
27
+ }
28
+ }
29
+ return parsed.length ? parsed : null;
30
+ }
31
+ /** Base class of every error thrown by the SDK. */
32
+ var DigisacError = class extends Error {
33
+ constructor(message, options) {
34
+ super(message, options);
35
+ this.name = "DigisacError";
36
+ }
37
+ };
38
+ /** The request never got an HTTP response (network failure, DNS, connection refused, ...). */
39
+ var ApiConnectionError = class extends DigisacError {
40
+ constructor(message = "Connection error.", options) {
41
+ super(message, options);
42
+ this.name = "ApiConnectionError";
43
+ }
44
+ };
45
+ /**
46
+ * The API answered with a non-2xx status. Thrown as the subclass matching the status
47
+ * (`NotFoundError`, `BadRequestError`, ...) — or `ValidationError` for a 400 field validation
48
+ * failure — so callers can narrow with `instanceof`.
49
+ */
50
+ var ApiError = class extends DigisacError {
51
+ /** The backend error name (`error` in the body), e.g. `ValidationError` or `PaymentRequired`. */
52
+ errorClass;
53
+ status;
54
+ /** Field errors of a 400 `ValidationError`; `null` when the response carries none. */
55
+ validationErrors;
56
+ headers;
57
+ /** The parsed JSON error body (`{ error, message, status, errors?, extra? }`), or the raw text. */
58
+ body;
59
+ constructor(message, errorClass, status, validationErrors = null, options = {}) {
60
+ super(message);
61
+ this.name = "ApiError";
62
+ this.errorClass = errorClass;
63
+ this.status = status;
64
+ this.validationErrors = validationErrors;
65
+ this.headers = options.headers;
66
+ this.body = options.body;
67
+ }
68
+ /**
69
+ * Builds the `ApiError` subclass for a response and its (already read) body: `ValidationError`
70
+ * for a 400 `ValidationError`, else the class of the status.
71
+ */
72
+ static fromResponse(status, statusText, headers, body) {
73
+ let message = `HTTP Error ${status}: ${statusText}`;
74
+ let errorClass;
75
+ let validationErrors = null;
76
+ if (isRecord(body)) {
77
+ const apiMessage = typeof body.message === "string" ? body.message : void 0;
78
+ errorClass = typeof body.error === "string" ? body.error : void 0;
79
+ message = apiMessage || errorClass || message;
80
+ validationErrors = parseFieldErrors(body.errors);
81
+ }
82
+ return new (errorClassFor(status, errorClass))(message, errorClass, status, validationErrors, {
83
+ headers,
84
+ body
85
+ });
86
+ }
87
+ };
88
+ /** 400 (see {@link ValidationError} for field validation failures). */
89
+ var BadRequestError = class extends ApiError {
90
+ name = "BadRequestError";
91
+ };
92
+ /**
93
+ * 400 `ValidationError` — the request failed field validation. `validationErrors` is always an
94
+ * array here (empty when the backend sent no field details).
95
+ */
96
+ var ValidationError = class extends BadRequestError {
97
+ name = "ValidationError";
98
+ constructor(message, errorClass, status, validationErrors = null, options = {}) {
99
+ super(message, errorClass, status, validationErrors ?? [], options);
100
+ }
101
+ };
102
+ /** 401 — missing or invalid access token. */
103
+ var AuthenticationError = class extends ApiError {
104
+ name = "AuthenticationError";
105
+ };
106
+ /** 402 — the account is out of credits or over a plan limit. */
107
+ var PaymentRequiredError = class extends ApiError {
108
+ name = "PaymentRequiredError";
109
+ };
110
+ /** 403 — the user lacks the permission for this action. */
111
+ var PermissionDeniedError = class extends ApiError {
112
+ name = "PermissionDeniedError";
113
+ };
114
+ /** 404 */
115
+ var NotFoundError = class extends ApiError {
116
+ name = "NotFoundError";
117
+ };
118
+ /** 409 */
119
+ var ConflictError = class extends ApiError {
120
+ name = "ConflictError";
121
+ };
122
+ /** 422 */
123
+ var UnprocessableEntityError = class extends ApiError {
124
+ name = "UnprocessableEntityError";
125
+ };
126
+ /** 429 */
127
+ var RateLimitError = class extends ApiError {
128
+ name = "RateLimitError";
129
+ /**
130
+ * Seconds to wait before retrying, or `null` when the response does not say.
131
+ *
132
+ * Reads the standard `Retry-After` header (delay seconds or HTTP date) first, then the
133
+ * `X-RateLimit-Reset` header the Digisac backend sends (Unix time in seconds).
134
+ */
135
+ get retryAfter() {
136
+ return parseRetryAfter(this.headers?.get("Retry-After")) ?? parseRateLimitReset(this.headers?.get("X-RateLimit-Reset"));
137
+ }
138
+ };
139
+ const secondsUntil = (epochMs) => Math.max(0, Math.ceil((epochMs - Date.now()) / 1e3));
140
+ /** `Retry-After`: delay seconds or an HTTP date. */
141
+ function parseRetryAfter(value) {
142
+ if (!value?.trim()) return null;
143
+ const seconds = Number(value);
144
+ if (Number.isFinite(seconds)) return Math.max(0, seconds);
145
+ const date = Date.parse(value);
146
+ return Number.isNaN(date) ? null : secondsUntil(date);
147
+ }
148
+ /**
149
+ * `X-RateLimit-Reset`: Unix time in seconds (what the Digisac backend sends). Small values are
150
+ * read as delay seconds instead, the other convention in use for this header.
151
+ */
152
+ function parseRateLimitReset(value) {
153
+ if (!value?.trim()) return null;
154
+ const reset = Number(value);
155
+ if (!Number.isFinite(reset)) return null;
156
+ return reset < 1e9 ? Math.max(0, reset) : secondsUntil(reset * 1e3);
157
+ }
158
+ /** 5xx */
159
+ var InternalServerError = class extends ApiError {
160
+ name = "InternalServerError";
161
+ };
162
+ function errorClassFor(status, errorClass) {
163
+ if (status === 400 && errorClass === "ValidationError") return ValidationError;
164
+ switch (status) {
165
+ case 400: return BadRequestError;
166
+ case 401: return AuthenticationError;
167
+ case 402: return PaymentRequiredError;
168
+ case 403: return PermissionDeniedError;
169
+ case 404: return NotFoundError;
170
+ case 409: return ConflictError;
171
+ case 422: return UnprocessableEntityError;
172
+ case 429: return RateLimitError;
173
+ default: return status >= 500 ? InternalServerError : ApiError;
174
+ }
175
+ }
176
+
177
+ //#endregion
178
+ exports.ApiConnectionError = ApiConnectionError;
179
+ exports.ApiError = ApiError;
180
+ exports.AuthenticationError = AuthenticationError;
181
+ exports.BadRequestError = BadRequestError;
182
+ exports.ConflictError = ConflictError;
183
+ exports.DigisacError = DigisacError;
184
+ exports.InternalServerError = InternalServerError;
185
+ exports.NotFoundError = NotFoundError;
186
+ exports.PaymentRequiredError = PaymentRequiredError;
187
+ exports.PermissionDeniedError = PermissionDeniedError;
188
+ exports.RateLimitError = RateLimitError;
189
+ exports.UnprocessableEntityError = UnprocessableEntityError;
190
+ exports.ValidationError = ValidationError;
191
+ exports.parseFieldErrors = parseFieldErrors;
192
+ //# sourceMappingURL=errors.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.cjs","names":[],"sources":["../../src/core/errors.ts"],"sourcesContent":["/**\n * A field-level error from the backend `validate` middleware — one entry per failed rule.\n */\nexport interface FieldError {\n /** Where the field was read from: `body`, `params`, `query`, `headers` or `cookies`. */\n location: string\n /** The field name, e.g. `name`. */\n field: string\n /** `<location>.<field>`, e.g. `body.name`. */\n path: string\n message: string\n /** The failed rule, e.g. `required` or `hasLengthLesserThanOrEqual`. */\n code: string\n}\n\n/**\n * The backend error name sent as `error` in the body (`ApiError.errorClass`). Known names are listed\n * for autocomplete; any other string is still accepted. `HttpError` is the generic one: most 400s\n * and every unexpected 500.\n */\nexport type ErrorClassName =\n | 'HttpError'\n | 'ValidationError'\n | 'BadRequestHttpError'\n | 'UnauthorizedHttpError'\n | 'PaymentRequired'\n | 'ForbiddenHttpError'\n | 'NotFoundHttpError'\n | 'TooManyRequestsHttpError'\n | (string & {})\n\nexport interface ApiErrorOptions {\n /** Response headers. */\n headers?: Headers\n /** The parsed JSON error body, or the raw text when it is not JSON. */\n body?: unknown\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Flattens the backend `errors` object — `{ <location>: { <field>: { messages: string[], types: string[] } } }`\n * — into one {@link FieldError} per message. Returns `null` when there are no field errors.\n */\nexport function parseFieldErrors(errors: unknown): FieldError[] | null {\n if (!isRecord(errors)) {\n return null\n }\n\n const parsed: FieldError[] = []\n for (const [location, fields] of Object.entries(errors)) {\n if (!isRecord(fields)) {\n continue\n }\n for (const [field, detail] of Object.entries(fields)) {\n if (!isRecord(detail)) {\n continue\n }\n const messages = Array.isArray(detail.messages) ? detail.messages.map(String) : []\n const types = Array.isArray(detail.types) ? detail.types.map(String) : []\n messages.forEach((message, index) => {\n parsed.push({\n location,\n field,\n path: `${location}.${field}`,\n message,\n code: types[index] ?? types[0] ?? '',\n })\n })\n }\n }\n\n return parsed.length ? parsed : null\n}\n\n/** Base class of every error thrown by the SDK. */\nexport class DigisacError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'DigisacError'\n }\n}\n\n/** The request never got an HTTP response (network failure, DNS, connection refused, ...). */\nexport class ApiConnectionError extends DigisacError {\n constructor(message = 'Connection error.', options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'ApiConnectionError'\n }\n}\n\n/**\n * The API answered with a non-2xx status. Thrown as the subclass matching the status\n * (`NotFoundError`, `BadRequestError`, ...) — or `ValidationError` for a 400 field validation\n * failure — so callers can narrow with `instanceof`.\n */\nexport class ApiError extends DigisacError {\n /** The backend error name (`error` in the body), e.g. `ValidationError` or `PaymentRequired`. */\n public readonly errorClass?: ErrorClassName | undefined\n public readonly status?: number | undefined\n /** Field errors of a 400 `ValidationError`; `null` when the response carries none. */\n public readonly validationErrors: FieldError[] | null\n public readonly headers?: Headers | undefined\n /** The parsed JSON error body (`{ error, message, status, errors?, extra? }`), or the raw text. */\n public readonly body?: unknown\n\n constructor(\n message: string,\n errorClass?: ErrorClassName,\n status?: number,\n validationErrors: FieldError[] | null = null,\n options: ApiErrorOptions = {},\n ) {\n super(message)\n this.name = 'ApiError'\n this.errorClass = errorClass\n this.status = status\n this.validationErrors = validationErrors\n this.headers = options.headers\n this.body = options.body\n }\n\n /**\n * Builds the `ApiError` subclass for a response and its (already read) body: `ValidationError`\n * for a 400 `ValidationError`, else the class of the status.\n */\n static fromResponse(\n status: number,\n statusText: string,\n headers: Headers,\n body: unknown,\n ): ApiError {\n let message = `HTTP Error ${status}: ${statusText}`\n let errorClass: string | undefined\n let validationErrors: FieldError[] | null = null\n\n if (isRecord(body)) {\n const apiMessage = typeof body.message === 'string' ? body.message : undefined\n errorClass = typeof body.error === 'string' ? body.error : undefined\n message = apiMessage || errorClass || message\n validationErrors = parseFieldErrors(body.errors)\n }\n\n const ErrorClass = errorClassFor(status, errorClass)\n return new ErrorClass(message, errorClass, status, validationErrors, { headers, body })\n }\n}\n\n/** 400 (see {@link ValidationError} for field validation failures). */\nexport class BadRequestError extends ApiError {\n override name = 'BadRequestError'\n}\n\n/**\n * 400 `ValidationError` — the request failed field validation. `validationErrors` is always an\n * array here (empty when the backend sent no field details).\n */\nexport class ValidationError extends BadRequestError {\n override name = 'ValidationError'\n declare public readonly validationErrors: FieldError[]\n\n constructor(\n message: string,\n errorClass?: ErrorClassName,\n status?: number,\n validationErrors: FieldError[] | null = null,\n options: ApiErrorOptions = {},\n ) {\n super(message, errorClass, status, validationErrors ?? [], options)\n }\n}\n\n/** 401 — missing or invalid access token. */\nexport class AuthenticationError extends ApiError {\n override name = 'AuthenticationError'\n}\n\n/** 402 — the account is out of credits or over a plan limit. */\nexport class PaymentRequiredError extends ApiError {\n override name = 'PaymentRequiredError'\n}\n\n/** 403 — the user lacks the permission for this action. */\nexport class PermissionDeniedError extends ApiError {\n override name = 'PermissionDeniedError'\n}\n\n/** 404 */\nexport class NotFoundError extends ApiError {\n override name = 'NotFoundError'\n}\n\n/** 409 */\nexport class ConflictError extends ApiError {\n override name = 'ConflictError'\n}\n\n/** 422 */\nexport class UnprocessableEntityError extends ApiError {\n override name = 'UnprocessableEntityError'\n}\n\n/** 429 */\nexport class RateLimitError extends ApiError {\n override name = 'RateLimitError'\n\n /**\n * Seconds to wait before retrying, or `null` when the response does not say.\n *\n * Reads the standard `Retry-After` header (delay seconds or HTTP date) first, then the\n * `X-RateLimit-Reset` header the Digisac backend sends (Unix time in seconds).\n */\n get retryAfter(): number | null {\n return (\n parseRetryAfter(this.headers?.get('Retry-After')) ??\n parseRateLimitReset(this.headers?.get('X-RateLimit-Reset'))\n )\n }\n}\n\nconst secondsUntil = (epochMs: number): number =>\n Math.max(0, Math.ceil((epochMs - Date.now()) / 1000))\n\n/** `Retry-After`: delay seconds or an HTTP date. */\nfunction parseRetryAfter(value: string | null | undefined): number | null {\n if (!value?.trim()) {\n return null\n }\n const seconds = Number(value)\n if (Number.isFinite(seconds)) {\n return Math.max(0, seconds)\n }\n const date = Date.parse(value)\n return Number.isNaN(date) ? null : secondsUntil(date)\n}\n\n/**\n * `X-RateLimit-Reset`: Unix time in seconds (what the Digisac backend sends). Small values are\n * read as delay seconds instead, the other convention in use for this header.\n */\nfunction parseRateLimitReset(value: string | null | undefined): number | null {\n if (!value?.trim()) {\n return null\n }\n const reset = Number(value)\n if (!Number.isFinite(reset)) {\n return null\n }\n // Anything before 2001-09-09 (1e9 s) can't be a reset time, so it is a delay.\n return reset < 1e9 ? Math.max(0, reset) : secondsUntil(reset * 1000)\n}\n\n/** 5xx */\nexport class InternalServerError extends ApiError {\n override name = 'InternalServerError'\n}\n\nfunction errorClassFor(status: number, errorClass: string | undefined): typeof ApiError {\n if (status === 400 && errorClass === 'ValidationError') {\n return ValidationError\n }\n\n switch (status) {\n case 400:\n return BadRequestError\n case 401:\n return AuthenticationError\n case 402:\n return PaymentRequiredError\n case 403:\n return PermissionDeniedError\n case 404:\n return NotFoundError\n case 409:\n return ConflictError\n case 422:\n return UnprocessableEntityError\n case 429:\n return RateLimitError\n default:\n return status >= 500 ? InternalServerError : ApiError\n }\n}\n"],"mappings":"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"}