apimo.js 1.0.4 → 1.1.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 +173 -191
- package/dist/core/api.d.ts +155 -131
- package/dist/core/api.js +124 -84
- package/dist/errors/index.d.ts +176 -0
- package/dist/errors/index.js +234 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/schemas/agency.d.ts +50 -359
- package/dist/schemas/common.d.ts +18 -111
- package/dist/schemas/internal.d.ts +2 -2
- package/dist/schemas/internal.js +4 -4
- package/dist/schemas/property.d.ts +250 -1373
- package/dist/services/storage/dummy.cache.js +5 -20
- package/dist/services/storage/filesystem.cache.js +41 -58
- package/dist/services/storage/memory.cache.js +31 -46
- package/package.json +11 -8
package/dist/core/api.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
1
|
import Bottleneck from 'bottleneck';
|
|
11
2
|
import { merge } from 'merge-anything';
|
|
12
3
|
import { z } from 'zod';
|
|
4
|
+
import { ApiConfigurationError, ApiResponseValidationError, ApiRetryExhaustedError, isRetryable, throwForStatus, } from '../errors';
|
|
13
5
|
import { getAgencySchema } from '../schemas/agency';
|
|
14
6
|
import { CatalogDefinitionSchema, CatalogEntrySchema } from '../schemas/common';
|
|
15
7
|
import { getPropertySchema } from '../schemas/property';
|
|
@@ -30,6 +22,11 @@ export const DEFAULT_ADDITIONAL_CONFIG = {
|
|
|
30
22
|
active: true,
|
|
31
23
|
},
|
|
32
24
|
},
|
|
25
|
+
retry: {
|
|
26
|
+
attempts: 3,
|
|
27
|
+
initialDelayMs: 200,
|
|
28
|
+
backoff: 'exponential',
|
|
29
|
+
},
|
|
33
30
|
};
|
|
34
31
|
export class Apimo {
|
|
35
32
|
constructor(
|
|
@@ -41,7 +38,23 @@ export class Apimo {
|
|
|
41
38
|
config = DEFAULT_ADDITIONAL_CONFIG) {
|
|
42
39
|
this.provider = provider;
|
|
43
40
|
this.token = token;
|
|
41
|
+
if (!provider || provider.trim() === '') {
|
|
42
|
+
throw new ApiConfigurationError('provider must be a non-empty string.');
|
|
43
|
+
}
|
|
44
|
+
if (!token || token.trim() === '') {
|
|
45
|
+
throw new ApiConfigurationError('token must be a non-empty string.');
|
|
46
|
+
}
|
|
44
47
|
this.config = merge(DEFAULT_ADDITIONAL_CONFIG, config);
|
|
48
|
+
if (!this.config.baseUrl || this.config.baseUrl.trim() === '') {
|
|
49
|
+
throw new ApiConfigurationError('baseUrl must be a non-empty string.');
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
// eslint-disable-next-line no-new
|
|
53
|
+
new URL(this.config.baseUrl);
|
|
54
|
+
}
|
|
55
|
+
catch (_a) {
|
|
56
|
+
throw new ApiConfigurationError(`baseUrl "${this.config.baseUrl}" is not a valid URL.`);
|
|
57
|
+
}
|
|
45
58
|
this.cache = this.config.catalogs.cache.active ? this.config.catalogs.cache.adapter : new DummyCache();
|
|
46
59
|
this.limiter = new Bottleneck({
|
|
47
60
|
reservoir: 10,
|
|
@@ -57,79 +70,108 @@ export class Apimo {
|
|
|
57
70
|
const extendedInit = Object.assign(Object.assign({}, init), { headers: Object.assign({ Authorization: `Basic ${btoa(`${this.provider}:${this.token}`)}` }, init === null || init === void 0 ? void 0 : init.headers) });
|
|
58
71
|
return this.limiter.schedule(() => fetch(input, extendedInit));
|
|
59
72
|
}
|
|
60
|
-
get(path, schema, options) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
async get(path, schema, options) {
|
|
74
|
+
const url = makeApiUrl(path, this.config, Object.assign({ culture: this.config.culture }, options));
|
|
75
|
+
const { attempts, initialDelayMs, backoff } = this.config.retry;
|
|
76
|
+
let lastError;
|
|
77
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
78
|
+
try {
|
|
79
|
+
const response = await this.fetch(url);
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
let responseBody;
|
|
82
|
+
try {
|
|
83
|
+
responseBody = await response.json();
|
|
84
|
+
}
|
|
85
|
+
catch (_a) {
|
|
86
|
+
// The body wasn't JSON — leave responseBody as undefined
|
|
87
|
+
}
|
|
88
|
+
throwForStatus(response.status, url.toString(), responseBody);
|
|
89
|
+
}
|
|
90
|
+
const json = await response.json();
|
|
91
|
+
const result = await schema.safeParseAsync(json);
|
|
92
|
+
if (!result.success) {
|
|
93
|
+
throw new ApiResponseValidationError(url.toString(), result.error);
|
|
94
|
+
}
|
|
95
|
+
return result.data;
|
|
65
96
|
}
|
|
66
|
-
|
|
67
|
-
|
|
97
|
+
catch (error) {
|
|
98
|
+
lastError = error;
|
|
99
|
+
const hasMoreAttempts = attempt < attempts;
|
|
100
|
+
if (!isRetryable(error)) {
|
|
101
|
+
// Non-transient errors (4xx, schema failures, etc.) — propagate immediately
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
if (!hasMoreAttempts) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
await this.sleep(this.retryDelayMs(attempt, initialDelayMs, backoff));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
throw new ApiRetryExhaustedError(attempts, lastError);
|
|
68
111
|
}
|
|
69
|
-
fetchCatalogs() {
|
|
70
|
-
return
|
|
71
|
-
return this.get(['catalogs'], z.array(CatalogDefinitionSchema));
|
|
72
|
-
});
|
|
112
|
+
async fetchCatalogs() {
|
|
113
|
+
return this.get(['catalogs'], z.array(CatalogDefinitionSchema));
|
|
73
114
|
}
|
|
74
|
-
populateCache(catalogName, culture, id) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
});
|
|
115
|
+
async populateCache(catalogName, culture, id) {
|
|
116
|
+
const catalog = await this.fetchCatalog(catalogName, { culture });
|
|
117
|
+
await this.cache.setEntries(catalogName, culture, catalog);
|
|
118
|
+
if (id !== undefined) {
|
|
119
|
+
const queriedKey = catalog.find(({ id: entryId }) => entryId === id);
|
|
120
|
+
return queriedKey
|
|
121
|
+
? {
|
|
122
|
+
name: queriedKey.name,
|
|
123
|
+
namePlural: queriedKey.name_plurial,
|
|
124
|
+
}
|
|
125
|
+
: null;
|
|
126
|
+
}
|
|
88
127
|
}
|
|
89
|
-
getCatalogEntries(catalogName, options) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
async getCatalogEntries(catalogName, options) {
|
|
129
|
+
var _a, _b, _c;
|
|
130
|
+
try {
|
|
131
|
+
return await this.cache.getEntries(catalogName, (_a = options === null || options === void 0 ? void 0 : options.culture) !== null && _a !== void 0 ? _a : this.config.culture);
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
if (e instanceof CacheExpiredError) {
|
|
135
|
+
await this.populateCache(catalogName, (_b = options === null || options === void 0 ? void 0 : options.culture) !== null && _b !== void 0 ? _b : this.config.culture);
|
|
136
|
+
return this.cache.getEntries(catalogName, (_c = options === null || options === void 0 ? void 0 : options.culture) !== null && _c !== void 0 ? _c : this.config.culture);
|
|
94
137
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
yield this.populateCache(catalogName, (_b = options === null || options === void 0 ? void 0 : options.culture) !== null && _b !== void 0 ? _b : this.config.culture);
|
|
98
|
-
return this.cache.getEntries(catalogName, (_c = options === null || options === void 0 ? void 0 : options.culture) !== null && _c !== void 0 ? _c : this.config.culture);
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
throw e;
|
|
102
|
-
}
|
|
138
|
+
else {
|
|
139
|
+
throw e;
|
|
103
140
|
}
|
|
104
|
-
}
|
|
141
|
+
}
|
|
105
142
|
}
|
|
106
|
-
fetchCatalog(catalogName, options) {
|
|
107
|
-
return
|
|
108
|
-
return this.get(['catalogs', catalogName], z.array(CatalogEntrySchema), options);
|
|
109
|
-
});
|
|
143
|
+
async fetchCatalog(catalogName, options) {
|
|
144
|
+
return this.get(['catalogs', catalogName], z.array(CatalogEntrySchema), options);
|
|
110
145
|
}
|
|
111
|
-
fetchAgencies(options) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}));
|
|
119
|
-
});
|
|
146
|
+
async fetchAgencies(options) {
|
|
147
|
+
var _a;
|
|
148
|
+
return this.get(['agencies'], z.object({
|
|
149
|
+
total_items: z.number(),
|
|
150
|
+
agencies: getAgencySchema(this.getLocalizedCatalogTransformer((_a = options === null || options === void 0 ? void 0 : options.culture) !== null && _a !== void 0 ? _a : this.config.culture), this.config).array(),
|
|
151
|
+
timestamp: z.number(),
|
|
152
|
+
}));
|
|
120
153
|
}
|
|
121
|
-
fetchProperties(agencyId, options) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
154
|
+
async fetchProperties(agencyId, options) {
|
|
155
|
+
var _a;
|
|
156
|
+
return this.get(['agencies', agencyId.toString(), 'properties'], z.object({
|
|
157
|
+
total_items: z.number(),
|
|
158
|
+
timestamp: z.number(),
|
|
159
|
+
properties: getPropertySchema(this.getLocalizedCatalogTransformer((_a = options === null || options === void 0 ? void 0 : options.culture) !== null && _a !== void 0 ? _a : this.config.culture)).array(),
|
|
160
|
+
}), options);
|
|
161
|
+
}
|
|
162
|
+
/** Calculates the delay before the next retry attempt (1-based attempt index). */
|
|
163
|
+
retryDelayMs(attempt, initialDelayMs, backoff) {
|
|
164
|
+
switch (backoff) {
|
|
165
|
+
case 'exponential': return initialDelayMs * 2 ** (attempt - 1);
|
|
166
|
+
case 'linear': return initialDelayMs * attempt;
|
|
167
|
+
case 'fixed': return initialDelayMs;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
sleep(ms) {
|
|
171
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
130
172
|
}
|
|
131
173
|
getLocalizedCatalogTransformer(culture) {
|
|
132
|
-
return (catalogName, id) =>
|
|
174
|
+
return async (catalogName, id) => {
|
|
133
175
|
if (!this.config.catalogs.transform.active) {
|
|
134
176
|
return `${catalogName}.${id}`;
|
|
135
177
|
}
|
|
@@ -137,21 +179,19 @@ export class Apimo {
|
|
|
137
179
|
return this.config.catalogs.transform.transformFn(catalogName, culture, id);
|
|
138
180
|
}
|
|
139
181
|
return this.catalogTransformer(catalogName, culture, id);
|
|
140
|
-
}
|
|
182
|
+
};
|
|
141
183
|
}
|
|
142
|
-
catalogTransformer(catalogName, culture, id) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
184
|
+
async catalogTransformer(catalogName, culture, id) {
|
|
185
|
+
try {
|
|
186
|
+
return await this.cache.getEntry(catalogName, culture, id);
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
if (e instanceof CacheExpiredError) {
|
|
190
|
+
return await this.populateCache(catalogName, culture, id);
|
|
146
191
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return yield this.populateCache(catalogName, culture, id);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
throw e;
|
|
153
|
-
}
|
|
192
|
+
else {
|
|
193
|
+
throw e;
|
|
154
194
|
}
|
|
155
|
-
}
|
|
195
|
+
}
|
|
156
196
|
}
|
|
157
197
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { ZodError } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Base class for all Apimo errors.
|
|
4
|
+
* All errors thrown by the library extend this class, so you can catch them all with `catch (e) { if (e instanceof ApimoError) ... }`.
|
|
5
|
+
*/
|
|
6
|
+
export declare class ApimoError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Thrown when the Apimo API responds with a non-2xx HTTP status code.
|
|
11
|
+
*
|
|
12
|
+
* Prefer catching one of the more specific subclasses when you need to react
|
|
13
|
+
* differently per status code.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* try {
|
|
18
|
+
* await api.fetchProperties(agencyId)
|
|
19
|
+
* } catch (e) {
|
|
20
|
+
* if (e instanceof ApiHttpError) {
|
|
21
|
+
* console.error(`HTTP ${e.statusCode}: ${e.message}`)
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare class ApiHttpError extends ApimoError {
|
|
27
|
+
/** The HTTP status code returned by the API. */
|
|
28
|
+
readonly statusCode: number;
|
|
29
|
+
/** The URL that was requested. */
|
|
30
|
+
readonly url: string;
|
|
31
|
+
/** The raw response body, when it was possible to parse it. */
|
|
32
|
+
readonly responseBody?: unknown | undefined;
|
|
33
|
+
constructor(
|
|
34
|
+
/** The HTTP status code returned by the API. */
|
|
35
|
+
statusCode: number,
|
|
36
|
+
/** The human-readable error message, sourced from the response body when available. */
|
|
37
|
+
message: string,
|
|
38
|
+
/** The URL that was requested. */
|
|
39
|
+
url: string,
|
|
40
|
+
/** The raw response body, when it was possible to parse it. */
|
|
41
|
+
responseBody?: unknown | undefined);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Thrown when the request is malformed (HTTP 400).
|
|
45
|
+
*
|
|
46
|
+
* This usually indicates a bug in the library or invalid arguments passed to a method.
|
|
47
|
+
*/
|
|
48
|
+
export declare class ApiBadRequestError extends ApiHttpError {
|
|
49
|
+
constructor(url: string, responseBody?: unknown);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Thrown when the provided credentials are invalid (HTTP 401).
|
|
53
|
+
*
|
|
54
|
+
* Verify that the `provider` and `token` passed to `new Apimo(...)` are correct.
|
|
55
|
+
*/
|
|
56
|
+
export declare class ApiUnauthorizedError extends ApiHttpError {
|
|
57
|
+
constructor(url: string, responseBody?: unknown);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Thrown when the authenticated user does not have access to the requested resource (HTTP 403).
|
|
61
|
+
*
|
|
62
|
+
* This can happen when trying to access an agency or property that belongs to a different provider.
|
|
63
|
+
*/
|
|
64
|
+
export declare class ApiForbiddenError extends ApiHttpError {
|
|
65
|
+
constructor(url: string, responseBody?: unknown);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Thrown when the requested resource does not exist (HTTP 404).
|
|
69
|
+
*
|
|
70
|
+
* Double-check the agency ID or any other identifiers you are passing.
|
|
71
|
+
*/
|
|
72
|
+
export declare class ApiNotFoundError extends ApiHttpError {
|
|
73
|
+
constructor(url: string, responseBody?: unknown);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Thrown when the API rate limit has been exceeded (HTTP 429).
|
|
77
|
+
*
|
|
78
|
+
* The built-in `Bottleneck` limiter normally prevents this, but it can still
|
|
79
|
+
* occur if multiple `Apimo` instances are running concurrently against the
|
|
80
|
+
* same credentials.
|
|
81
|
+
*/
|
|
82
|
+
export declare class ApiRateLimitError extends ApiHttpError {
|
|
83
|
+
constructor(url: string, responseBody?: unknown);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Thrown when the Apimo API returns a server-side error (HTTP 5xx).
|
|
87
|
+
*
|
|
88
|
+
* This indicates a problem on Apimo's infrastructure; retrying after a delay is usually appropriate.
|
|
89
|
+
*/
|
|
90
|
+
export declare class ApiServerError extends ApiHttpError {
|
|
91
|
+
constructor(statusCode: number, url: string, responseBody?: unknown);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Thrown when the response body does not match the expected schema.
|
|
95
|
+
*
|
|
96
|
+
* This most commonly happens when the Apimo API changes its response shape
|
|
97
|
+
* without notice. The `zodError` property contains the full Zod validation
|
|
98
|
+
* details to help you pinpoint the mismatch.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* } catch (e) {
|
|
103
|
+
* if (e instanceof ApiResponseValidationError) {
|
|
104
|
+
* console.error('Schema mismatch at', e.url)
|
|
105
|
+
* console.error(e.zodError.format())
|
|
106
|
+
* }
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare class ApiResponseValidationError extends ApimoError {
|
|
111
|
+
/** The URL that was requested. */
|
|
112
|
+
readonly url: string;
|
|
113
|
+
/** The raw Zod error, containing detailed path/message information. */
|
|
114
|
+
readonly zodError: ZodError;
|
|
115
|
+
constructor(
|
|
116
|
+
/** The URL that was requested. */
|
|
117
|
+
url: string,
|
|
118
|
+
/** The raw Zod error, containing detailed path/message information. */
|
|
119
|
+
zodError: ZodError);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Thrown when the `Apimo` instance is configured incorrectly before any
|
|
123
|
+
* network request is even made.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* } catch (e) {
|
|
128
|
+
* if (e instanceof ApiConfigurationError) {
|
|
129
|
+
* console.error('Fix your Apimo config:', e.message)
|
|
130
|
+
* }
|
|
131
|
+
* }
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export declare class ApiConfigurationError extends ApimoError {
|
|
135
|
+
constructor(message: string);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Thrown when all retry attempts have been exhausted and the last attempt still
|
|
139
|
+
* failed. Inspect `cause` for the underlying error from the final attempt.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* } catch (e) {
|
|
144
|
+
* if (e instanceof ApiRetryExhaustedError) {
|
|
145
|
+
* console.error(`Gave up after ${e.attempts} attempt(s). Last error:`, e.cause)
|
|
146
|
+
* }
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export declare class ApiRetryExhaustedError extends ApimoError {
|
|
151
|
+
/** Total number of attempts made (including the first). */
|
|
152
|
+
readonly attempts: number;
|
|
153
|
+
/** The error thrown by the final attempt. */
|
|
154
|
+
readonly cause: unknown;
|
|
155
|
+
constructor(
|
|
156
|
+
/** Total number of attempts made (including the first). */
|
|
157
|
+
attempts: number,
|
|
158
|
+
/** The error thrown by the final attempt. */
|
|
159
|
+
cause: unknown);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Returns `true` for errors that are safe to retry (transient failures).
|
|
163
|
+
* 429 and 5xx HTTP errors are retryable; 4xx errors (except 429) are not.
|
|
164
|
+
* Schema validation errors are never retried (same response will always fail).
|
|
165
|
+
* Non-HTTP errors (network failures, etc.) are considered retryable.
|
|
166
|
+
*
|
|
167
|
+
* @internal
|
|
168
|
+
*/
|
|
169
|
+
export declare function isRetryable(error: unknown): boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Maps an HTTP status code to the most specific `ApiHttpError` subclass and
|
|
172
|
+
* throws it. Falls back to the generic `ApiHttpError` for unrecognised codes.
|
|
173
|
+
*
|
|
174
|
+
* @internal
|
|
175
|
+
*/
|
|
176
|
+
export declare function throwForStatus(statusCode: number, url: string, responseBody?: unknown): never;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all Apimo errors.
|
|
3
|
+
* All errors thrown by the library extend this class, so you can catch them all with `catch (e) { if (e instanceof ApimoError) ... }`.
|
|
4
|
+
*/
|
|
5
|
+
export class ApimoError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = this.constructor.name;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// HTTP / Network Errors
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/**
|
|
15
|
+
* Thrown when the Apimo API responds with a non-2xx HTTP status code.
|
|
16
|
+
*
|
|
17
|
+
* Prefer catching one of the more specific subclasses when you need to react
|
|
18
|
+
* differently per status code.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* try {
|
|
23
|
+
* await api.fetchProperties(agencyId)
|
|
24
|
+
* } catch (e) {
|
|
25
|
+
* if (e instanceof ApiHttpError) {
|
|
26
|
+
* console.error(`HTTP ${e.statusCode}: ${e.message}`)
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class ApiHttpError extends ApimoError {
|
|
32
|
+
constructor(
|
|
33
|
+
/** The HTTP status code returned by the API. */
|
|
34
|
+
statusCode,
|
|
35
|
+
/** The human-readable error message, sourced from the response body when available. */
|
|
36
|
+
message,
|
|
37
|
+
/** The URL that was requested. */
|
|
38
|
+
url,
|
|
39
|
+
/** The raw response body, when it was possible to parse it. */
|
|
40
|
+
responseBody) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.statusCode = statusCode;
|
|
43
|
+
this.url = url;
|
|
44
|
+
this.responseBody = responseBody;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Thrown when the request is malformed (HTTP 400).
|
|
49
|
+
*
|
|
50
|
+
* This usually indicates a bug in the library or invalid arguments passed to a method.
|
|
51
|
+
*/
|
|
52
|
+
export class ApiBadRequestError extends ApiHttpError {
|
|
53
|
+
constructor(url, responseBody) {
|
|
54
|
+
super(400, 'Bad request: the server could not understand the request. Check that all parameters are valid.', url, responseBody);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Thrown when the provided credentials are invalid (HTTP 401).
|
|
59
|
+
*
|
|
60
|
+
* Verify that the `provider` and `token` passed to `new Apimo(...)` are correct.
|
|
61
|
+
*/
|
|
62
|
+
export class ApiUnauthorizedError extends ApiHttpError {
|
|
63
|
+
constructor(url, responseBody) {
|
|
64
|
+
super(401, 'Unauthorized: invalid credentials. Verify your provider ID and token.', url, responseBody);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Thrown when the authenticated user does not have access to the requested resource (HTTP 403).
|
|
69
|
+
*
|
|
70
|
+
* This can happen when trying to access an agency or property that belongs to a different provider.
|
|
71
|
+
*/
|
|
72
|
+
export class ApiForbiddenError extends ApiHttpError {
|
|
73
|
+
constructor(url, responseBody) {
|
|
74
|
+
super(403, 'Forbidden: you do not have permission to access this resource.', url, responseBody);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Thrown when the requested resource does not exist (HTTP 404).
|
|
79
|
+
*
|
|
80
|
+
* Double-check the agency ID or any other identifiers you are passing.
|
|
81
|
+
*/
|
|
82
|
+
export class ApiNotFoundError extends ApiHttpError {
|
|
83
|
+
constructor(url, responseBody) {
|
|
84
|
+
super(404, 'Not found: the requested resource does not exist.', url, responseBody);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Thrown when the API rate limit has been exceeded (HTTP 429).
|
|
89
|
+
*
|
|
90
|
+
* The built-in `Bottleneck` limiter normally prevents this, but it can still
|
|
91
|
+
* occur if multiple `Apimo` instances are running concurrently against the
|
|
92
|
+
* same credentials.
|
|
93
|
+
*/
|
|
94
|
+
export class ApiRateLimitError extends ApiHttpError {
|
|
95
|
+
constructor(url, responseBody) {
|
|
96
|
+
super(429, 'Rate limit exceeded: too many requests. Slow down and retry after a moment. You may have hit your daily limit.', url, responseBody);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Thrown when the Apimo API returns a server-side error (HTTP 5xx).
|
|
101
|
+
*
|
|
102
|
+
* This indicates a problem on Apimo's infrastructure; retrying after a delay is usually appropriate.
|
|
103
|
+
*/
|
|
104
|
+
export class ApiServerError extends ApiHttpError {
|
|
105
|
+
constructor(statusCode, url, responseBody) {
|
|
106
|
+
super(statusCode, `Server error (${statusCode}): the Apimo API encountered an internal error. Try again later.`, url, responseBody);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Schema / Validation Errors
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
/**
|
|
113
|
+
* Thrown when the response body does not match the expected schema.
|
|
114
|
+
*
|
|
115
|
+
* This most commonly happens when the Apimo API changes its response shape
|
|
116
|
+
* without notice. The `zodError` property contains the full Zod validation
|
|
117
|
+
* details to help you pinpoint the mismatch.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* } catch (e) {
|
|
122
|
+
* if (e instanceof ApiResponseValidationError) {
|
|
123
|
+
* console.error('Schema mismatch at', e.url)
|
|
124
|
+
* console.error(e.zodError.format())
|
|
125
|
+
* }
|
|
126
|
+
* }
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export class ApiResponseValidationError extends ApimoError {
|
|
130
|
+
constructor(
|
|
131
|
+
/** The URL that was requested. */
|
|
132
|
+
url,
|
|
133
|
+
/** The raw Zod error, containing detailed path/message information. */
|
|
134
|
+
zodError) {
|
|
135
|
+
var _a, _b;
|
|
136
|
+
// Zod v4 uses `issues`; fall back to `errors` for Zod v3 compatibility.
|
|
137
|
+
const issues = (_b = (_a = zodError.issues) !== null && _a !== void 0 ? _a : zodError.errors) !== null && _b !== void 0 ? _b : [];
|
|
138
|
+
super(`Response validation failed for ${url}:\n${issues
|
|
139
|
+
.map(e => ` - [${e.path.join('.')}] ${e.message}`)
|
|
140
|
+
.join('\n')}`);
|
|
141
|
+
this.url = url;
|
|
142
|
+
this.zodError = zodError;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Configuration Errors
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
/**
|
|
149
|
+
* Thrown when the `Apimo` instance is configured incorrectly before any
|
|
150
|
+
* network request is even made.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* } catch (e) {
|
|
155
|
+
* if (e instanceof ApiConfigurationError) {
|
|
156
|
+
* console.error('Fix your Apimo config:', e.message)
|
|
157
|
+
* }
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export class ApiConfigurationError extends ApimoError {
|
|
162
|
+
constructor(message) {
|
|
163
|
+
super(`Configuration error: ${message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Retry Errors
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
/**
|
|
170
|
+
* Thrown when all retry attempts have been exhausted and the last attempt still
|
|
171
|
+
* failed. Inspect `cause` for the underlying error from the final attempt.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* } catch (e) {
|
|
176
|
+
* if (e instanceof ApiRetryExhaustedError) {
|
|
177
|
+
* console.error(`Gave up after ${e.attempts} attempt(s). Last error:`, e.cause)
|
|
178
|
+
* }
|
|
179
|
+
* }
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
export class ApiRetryExhaustedError extends ApimoError {
|
|
183
|
+
constructor(
|
|
184
|
+
/** Total number of attempts made (including the first). */
|
|
185
|
+
attempts,
|
|
186
|
+
/** The error thrown by the final attempt. */
|
|
187
|
+
cause) {
|
|
188
|
+
super(`Request failed after ${attempts} attempt${attempts === 1 ? '' : 's'}. Last error: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
189
|
+
this.attempts = attempts;
|
|
190
|
+
this.cause = cause;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Helpers
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
/**
|
|
197
|
+
* Returns `true` for errors that are safe to retry (transient failures).
|
|
198
|
+
* 429 and 5xx HTTP errors are retryable; 4xx errors (except 429) are not.
|
|
199
|
+
* Schema validation errors are never retried (same response will always fail).
|
|
200
|
+
* Non-HTTP errors (network failures, etc.) are considered retryable.
|
|
201
|
+
*
|
|
202
|
+
* @internal
|
|
203
|
+
*/
|
|
204
|
+
export function isRetryable(error) {
|
|
205
|
+
// Schema validation failures are deterministic — retrying will not help
|
|
206
|
+
if (error instanceof ApiResponseValidationError) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if (error instanceof ApiHttpError) {
|
|
210
|
+
return error.statusCode === 429 || error.statusCode >= 500;
|
|
211
|
+
}
|
|
212
|
+
// Network errors, timeouts, etc.
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Maps an HTTP status code to the most specific `ApiHttpError` subclass and
|
|
217
|
+
* throws it. Falls back to the generic `ApiHttpError` for unrecognised codes.
|
|
218
|
+
*
|
|
219
|
+
* @internal
|
|
220
|
+
*/
|
|
221
|
+
export function throwForStatus(statusCode, url, responseBody) {
|
|
222
|
+
switch (statusCode) {
|
|
223
|
+
case 400: throw new ApiBadRequestError(url, responseBody);
|
|
224
|
+
case 401: throw new ApiUnauthorizedError(url, responseBody);
|
|
225
|
+
case 403: throw new ApiForbiddenError(url, responseBody);
|
|
226
|
+
case 404: throw new ApiNotFoundError(url, responseBody);
|
|
227
|
+
case 429: throw new ApiRateLimitError(url, responseBody);
|
|
228
|
+
default:
|
|
229
|
+
if (statusCode >= 500) {
|
|
230
|
+
throw new ApiServerError(statusCode, url, responseBody);
|
|
231
|
+
}
|
|
232
|
+
throw new ApiHttpError(statusCode, `HTTP error ${statusCode}`, url, responseBody);
|
|
233
|
+
}
|
|
234
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type { CatalogName } from './consts/catalogs';
|
|
|
2
2
|
export type { ApiCulture } from './consts/languages';
|
|
3
3
|
export { type AdditionalConfig, Apimo, DEFAULT_ADDITIONAL_CONFIG, DEFAULT_BASE_URL } from './core/api';
|
|
4
4
|
export { Apimo as Api } from './core/api';
|
|
5
|
+
export { ApiBadRequestError, ApiConfigurationError, ApiForbiddenError, ApiHttpError, ApimoError, ApiNotFoundError, ApiRateLimitError, ApiResponseValidationError, ApiRetryExhaustedError, ApiServerError, ApiUnauthorizedError, } from './errors';
|
|
5
6
|
export type { ApimoAgency, ApimoPartner, ApimoRate } from './schemas/agency';
|
|
6
7
|
export type { ApimoCity, ApimoUser, CatalogDefinition, CatalogEntry, CatalogTransformer, LocalizedCatalogTransformer, } from './schemas/common';
|
|
7
8
|
export type { ApimoAgreement, ApimoArea, ApimoComment, ApimoConstruction, ApimoFloor, ApimoHeating, ApimoPicture, ApimoPlot, ApimoPrice, ApimoProperty, ApimoRegulation, ApimoResidence, ApimoSurface, ApimoView, ApimoWater, } from './schemas/property';
|