@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 +42 -7
- package/dist/core/BaseApiClient.cjs +25 -56
- package/dist/core/BaseApiClient.cjs.map +1 -1
- package/dist/core/BaseApiClient.d.ts +2 -11
- package/dist/core/BaseApiClient.d.ts.map +1 -1
- package/dist/core/BaseApiClient.mjs +27 -56
- package/dist/core/BaseApiClient.mjs.map +1 -1
- package/dist/core/errors.cjs +192 -0
- package/dist/core/errors.cjs.map +1 -0
- package/dist/core/errors.d.ts +117 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.mjs +177 -0
- package/dist/core/errors.mjs.map +1 -0
- package/dist/core/index.cjs +16 -2
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.mjs +3 -2
- package/dist/index.cjs +16 -2
- package/dist/index.mjs +3 -2
- package/package.json +1 -1
- package/src/core/BaseApiClient.test.ts +183 -22
- package/src/core/BaseApiClient.ts +37 -86
- package/src/core/BaseCrudApi.test-d.ts +48 -0
- package/src/core/BaseCrudApi.test.ts +150 -0
- package/src/core/errors.test-d.ts +14 -0
- package/src/core/errors.test.ts +389 -0
- package/src/core/errors.ts +284 -0
- package/src/core/index.ts +18 -2
package/README.md
CHANGED
|
@@ -165,23 +165,58 @@ const result = await people.getMany({
|
|
|
165
165
|
|
|
166
166
|
## Error Handling
|
|
167
167
|
|
|
168
|
-
|
|
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 {
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
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'\
|
|
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;
|
|
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
|
|
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 {
|
|
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'\
|
|
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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"}
|