apimo.js 1.0.4 → 1.0.5

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.
@@ -26,6 +26,27 @@ export interface AdditionalConfig {
26
26
  transformFn?: CatalogTransformer;
27
27
  };
28
28
  };
29
+ retry: {
30
+ /**
31
+ * Maximum total number of attempts (1 = no retries, 2 = one retry, etc.).
32
+ * @default 3
33
+ */
34
+ attempts: number;
35
+ /**
36
+ * Delay in milliseconds before the first retry.
37
+ * Subsequent delays are calculated according to the `backoff` strategy.
38
+ * @default 200
39
+ */
40
+ initialDelayMs: number;
41
+ /**
42
+ * Back-off strategy applied between attempts.
43
+ * - `exponential` — delay doubles on every retry (200 → 400 → 800 …)
44
+ * - `linear` — delay increases by `initialDelayMs` each time (200 → 400 → 600 …)
45
+ * - `fixed` — the same delay is used for every retry
46
+ * @default 'exponential'
47
+ */
48
+ backoff: 'exponential' | 'linear' | 'fixed';
49
+ };
29
50
  }
30
51
  export declare const DEFAULT_BASE_URL = "https://api.apimo.pro";
31
52
  export declare const DEFAULT_ADDITIONAL_CONFIG: AdditionalConfig;
@@ -47,31 +68,42 @@ export declare class Apimo {
47
68
  getCatalogEntries(catalogName: CatalogName, options?: Pick<ApiSearchParams, 'culture'>): Promise<CatalogEntry[]>;
48
69
  fetchCatalog(catalogName: CatalogName, options?: Pick<ApiSearchParams, 'culture'>): Promise<CatalogEntry[]>;
49
70
  fetchAgencies(options?: Pick<ApiSearchParams, 'culture' | 'limit' | 'offset'>): Promise<{
50
- timestamp: number;
71
+ total_items: number;
51
72
  agencies: {
52
73
  id: number;
53
- name: string;
74
+ reference: number;
54
75
  active: boolean;
55
- created_at: Date;
56
- updated_at: Date;
57
- email: string;
58
- phone: string;
59
- fax: string | null;
76
+ name: string;
77
+ company: {
78
+ id: number;
79
+ name: string;
80
+ };
81
+ brand: unknown;
82
+ networks: unknown[];
83
+ address: string;
84
+ address_more: string | null;
60
85
  city: {
61
86
  id: number;
62
87
  name: string;
63
88
  zipcode: string;
64
89
  };
90
+ district: unknown;
91
+ country: string;
92
+ region: string;
93
+ latitude: number;
94
+ longitude: number;
95
+ email: string;
96
+ phone: string;
97
+ fax: string | null;
98
+ url: string;
99
+ logo: string;
100
+ logo_svg: string | null;
65
101
  picture: string;
66
- partners: {
67
- name: string | null;
68
- type: number;
69
- partner: number | null;
70
- reference: string;
71
- amount: number;
72
- currency: string;
73
- }[];
74
- stories: unknown[];
102
+ currency: string;
103
+ timetable: string;
104
+ created_at: Date;
105
+ updated_at: Date;
106
+ providers: string;
75
107
  rates: {
76
108
  id: number;
77
109
  category: string | CatalogEntryName | null;
@@ -82,24 +114,15 @@ export declare class Apimo {
82
114
  comment: string;
83
115
  url: string | null;
84
116
  }[];
85
- url: string;
86
- reference: number;
87
- currency: string;
88
- company: {
89
- id: number;
90
- name: string;
91
- };
92
- networks: unknown[];
93
- address: string;
94
- address_more: string | null;
95
- country: string;
96
- region: string;
97
- latitude: number;
98
- longitude: number;
99
- logo: string;
100
- logo_svg: string | null;
101
- timetable: string;
102
- providers: string;
117
+ partners: {
118
+ type: number;
119
+ partner: number | null;
120
+ name: string | null;
121
+ reference: string;
122
+ amount: number;
123
+ currency: string;
124
+ }[];
125
+ stories: unknown[];
103
126
  users: {
104
127
  id: number;
105
128
  agency: number;
@@ -117,6 +140,7 @@ export declare class Apimo {
117
140
  birthday_at: Date;
118
141
  timezone: string | null;
119
142
  picture: string | null;
143
+ rates: unknown;
120
144
  username?: string | undefined;
121
145
  password?: string | undefined;
122
146
  spoken_languages?: string[] | undefined;
@@ -126,75 +150,22 @@ export declare class Apimo {
126
150
  } | null | undefined;
127
151
  partners?: unknown[] | undefined;
128
152
  stories?: unknown[] | undefined;
129
- rates?: unknown;
130
153
  }[];
131
154
  sectors: unknown[];
132
155
  parameters: string;
133
156
  subscription: string;
134
- brand?: unknown;
135
- district?: unknown;
136
157
  }[];
137
- total_items: number;
158
+ timestamp: number;
138
159
  }>;
139
160
  fetchProperties(agencyId: number, options?: Pick<ApiSearchParams, 'culture' | 'limit' | 'offset' | 'timestamp' | 'step' | 'status' | 'group'>): Promise<{
140
- timestamp: number;
141
161
  total_items: number;
162
+ timestamp: number;
142
163
  properties: {
143
- tags: (string | CatalogEntryName | null)[];
144
164
  id: number;
145
- name: string | null;
146
- options: unknown[];
147
- type: string | CatalogEntryName | null;
148
- length: number | null;
149
- status: string | CatalogEntryName | null;
150
- agency: number;
151
- created_at: Date;
152
- updated_at: Date;
153
- city: {
154
- id: number;
155
- name: string;
156
- zipcode: string;
157
- };
158
- rates: unknown[];
159
- step: string | CatalogEntryName | null;
160
- category: string | CatalogEntryName | null;
161
- url: string | null;
162
165
  reference: number;
163
- address: string | null;
164
- address_more: string | null;
165
- district: {
166
- id: number;
167
- name: string;
168
- } | null;
169
- country: string;
170
- region: {
171
- id: number;
172
- name: string;
173
- };
174
- latitude: number;
175
- longitude: number;
176
- height: number | null;
177
- comments: {
178
- language: string;
179
- comment: string;
180
- title?: string | null | undefined;
181
- subtitle?: string | null | undefined;
182
- hook?: unknown;
183
- comment_full?: string | null | undefined;
184
- }[];
185
- area: {
186
- value: number;
187
- unit: string | CatalogEntryName | null;
188
- total: number;
189
- weighted: number;
190
- };
191
- floor: {
192
- value: number;
193
- type: string | CatalogEntryName | null;
194
- levels: number;
195
- floors: number;
196
- };
197
- orientations: (string | CatalogEntryName | null)[];
166
+ agency: number;
167
+ brand: unknown;
168
+ sector: unknown;
198
169
  user: {
199
170
  id: number;
200
171
  agency: number;
@@ -212,6 +183,7 @@ export declare class Apimo {
212
183
  birthday_at: Date;
213
184
  timezone: string | null;
214
185
  picture: string | null;
186
+ rates: unknown;
215
187
  username?: string | undefined;
216
188
  password?: string | undefined;
217
189
  spoken_languages?: string[] | undefined;
@@ -221,9 +193,14 @@ export declare class Apimo {
221
193
  } | null | undefined;
222
194
  partners?: unknown[] | undefined;
223
195
  stories?: unknown[] | undefined;
224
- rates?: unknown;
225
196
  };
197
+ step: string | CatalogEntryName | null;
198
+ status: string | CatalogEntryName | null;
226
199
  parent: number | null;
200
+ ranking: unknown;
201
+ category: string | CatalogEntryName | null;
202
+ name: string | null;
203
+ type: string | CatalogEntryName | null;
227
204
  subtype: string | CatalogEntryName | null;
228
205
  agreement: {
229
206
  type: string | CatalogEntryName | null;
@@ -235,9 +212,38 @@ export declare class Apimo {
235
212
  lot_reference: string | null;
236
213
  cadastre_reference: string | null;
237
214
  stairs_reference: string | null;
215
+ address: string | null;
216
+ address_more: string | null;
238
217
  publish_address: boolean;
218
+ country: string;
219
+ region: {
220
+ id: number;
221
+ name: string;
222
+ };
223
+ city: {
224
+ id: number;
225
+ name: string;
226
+ zipcode: string;
227
+ };
228
+ original_city: unknown;
229
+ district: {
230
+ id: number;
231
+ name: string;
232
+ } | null;
233
+ original_district: unknown;
234
+ location: unknown;
235
+ longitude: number;
236
+ latitude: number;
239
237
  radius: number;
240
238
  altitude: number;
239
+ referral: unknown;
240
+ subreferral: unknown;
241
+ area: {
242
+ unit: string | CatalogEntryName | null;
243
+ value: number;
244
+ total: number;
245
+ weighted: number;
246
+ };
241
247
  plot: {
242
248
  net_floor: number;
243
249
  land_type: string | CatalogEntryName | null;
@@ -249,26 +255,29 @@ export declare class Apimo {
249
255
  bedrooms: number;
250
256
  sleeps: number;
251
257
  price: {
252
- fees: number;
253
258
  value: number;
254
- currency: string;
255
259
  max: number;
260
+ fees: number;
261
+ unit: unknown;
256
262
  period: string | CatalogEntryName | null;
257
263
  hide: boolean;
258
264
  inventory: number | null;
259
265
  deposit: number | null;
266
+ currency: string;
260
267
  commission: number | null;
268
+ transfer_tax: unknown;
269
+ contribution: unknown;
270
+ pension: unknown;
261
271
  tenant: number | null;
262
272
  vat: boolean | null;
263
- unit?: unknown;
264
- transfer_tax?: unknown;
265
- contribution?: unknown;
266
- pension?: unknown;
267
273
  };
274
+ rates: unknown[];
275
+ owner: unknown;
276
+ visit: unknown;
268
277
  residence: {
269
- fees: number;
270
278
  id: number;
271
279
  type: string | CatalogEntryName | null;
280
+ fees: number;
272
281
  period: string | CatalogEntryName | null;
273
282
  lots: number;
274
283
  } | null;
@@ -277,17 +286,23 @@ export declare class Apimo {
277
286
  landscape: (string | CatalogEntryName | null)[];
278
287
  } | null;
279
288
  construction: {
280
- construction_step: string | CatalogEntryName | null;
281
289
  construction_year: number;
282
290
  renovation_year: number;
283
291
  renovation_cost: number;
292
+ construction_step: string | CatalogEntryName | null;
284
293
  type?: (string | CatalogEntryName | null)[] | undefined;
285
294
  };
286
- heating: {
295
+ floor: {
287
296
  type: string | CatalogEntryName | null;
297
+ value: number;
298
+ levels: number;
299
+ floors: number;
300
+ };
301
+ heating: {
288
302
  device: string | CatalogEntryName | null;
289
303
  devices: (string | CatalogEntryName | null)[] | null;
290
304
  access: string | CatalogEntryName | null;
305
+ type: string | CatalogEntryName | null;
291
306
  types: (string | CatalogEntryName | null)[] | null;
292
307
  };
293
308
  water: {
@@ -302,22 +317,29 @@ export declare class Apimo {
302
317
  };
303
318
  twinned: number | null;
304
319
  facades: number;
320
+ length: number | null;
321
+ height: number | null;
322
+ url: string | null;
305
323
  availability: string | CatalogEntryName | null;
324
+ available_at: unknown;
306
325
  delivered_at: Date | null;
307
326
  activities: (string | CatalogEntryName | null)[];
327
+ orientations: (string | CatalogEntryName | null)[];
308
328
  services: (string | CatalogEntryName | null)[];
309
329
  proximities: (string | CatalogEntryName | null)[];
330
+ tags: (string | CatalogEntryName | null)[];
310
331
  tags_customized: unknown[];
311
332
  pictures: {
312
333
  id: number;
313
- url: string;
314
334
  rank: number;
335
+ url: string;
315
336
  width_max: number;
316
337
  height_max: number;
317
338
  internet: boolean;
318
339
  print: boolean;
319
340
  panorama: boolean;
320
341
  child: number;
342
+ reference: unknown;
321
343
  comments: {
322
344
  language: string;
323
345
  comment: string;
@@ -326,13 +348,28 @@ export declare class Apimo {
326
348
  hook?: unknown;
327
349
  comment_full?: string | null | undefined;
328
350
  }[];
329
- reference?: unknown;
330
351
  }[];
331
352
  medias: unknown[];
332
353
  documents: unknown[];
354
+ comments: {
355
+ language: string;
356
+ comment: string;
357
+ title?: string | null | undefined;
358
+ subtitle?: string | null | undefined;
359
+ hook?: unknown;
360
+ comment_full?: string | null | undefined;
361
+ }[];
333
362
  areas: {
334
- number: number;
335
363
  type: string | CatalogEntryName | null;
364
+ number: number;
365
+ area: number;
366
+ flooring: string | CatalogEntryName | null;
367
+ ceiling_height: number | null;
368
+ floor: {
369
+ type: string | CatalogEntryName | null;
370
+ value: number;
371
+ };
372
+ orientations: (string | CatalogEntryName | null)[];
336
373
  comments: {
337
374
  language: string;
338
375
  comment: string;
@@ -341,49 +378,36 @@ export declare class Apimo {
341
378
  hook?: unknown;
342
379
  comment_full?: string | null | undefined;
343
380
  }[];
344
- area: number;
345
- flooring: string | CatalogEntryName | null;
346
- ceiling_height: number | null;
347
- floor: {
348
- value: number;
349
- type: string | CatalogEntryName | null;
350
- };
351
- orientations: (string | CatalogEntryName | null)[];
352
381
  lot: {
382
+ type: unknown;
383
+ rank: unknown;
353
384
  name: unknown[];
354
- type?: unknown;
355
- rank?: unknown;
356
385
  };
357
386
  }[];
358
387
  regulations: {
359
- value: number[];
360
388
  type: string | CatalogEntryName | null;
389
+ value: number[];
361
390
  date: Date | null;
362
391
  graph: string | null;
363
392
  }[];
364
393
  financial: unknown[];
365
394
  exchanges: unknown[];
395
+ options: unknown[];
396
+ filling_rate: unknown;
397
+ private_comment: unknown;
398
+ interagency_comment: unknown;
399
+ status_comment: unknown;
366
400
  logs: unknown[];
367
401
  referrals: unknown[];
402
+ created_at: Date;
403
+ updated_at: Date;
368
404
  created_by: number;
369
405
  updated_by: number;
370
- referral?: unknown;
371
- brand?: unknown;
372
- sector?: unknown;
373
- ranking?: unknown;
374
- original_city?: unknown;
375
- original_district?: unknown;
376
- location?: unknown;
377
- subreferral?: unknown;
378
- owner?: unknown;
379
- visit?: unknown;
380
- available_at?: unknown;
381
- filling_rate?: unknown;
382
- private_comment?: unknown;
383
- interagency_comment?: unknown;
384
- status_comment?: unknown;
385
406
  }[];
386
407
  }>;
408
+ /** Calculates the delay before the next retry attempt (1-based attempt index). */
409
+ private retryDelayMs;
410
+ private sleep;
387
411
  private getLocalizedCatalogTransformer;
388
412
  private catalogTransformer;
389
413
  }
package/dist/core/api.js CHANGED
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import Bottleneck from 'bottleneck';
11
11
  import { merge } from 'merge-anything';
12
12
  import { z } from 'zod';
13
+ import { ApiConfigurationError, ApiResponseValidationError, ApiRetryExhaustedError, isRetryable, throwForStatus, } from '../errors';
13
14
  import { getAgencySchema } from '../schemas/agency';
14
15
  import { CatalogDefinitionSchema, CatalogEntrySchema } from '../schemas/common';
15
16
  import { getPropertySchema } from '../schemas/property';
@@ -30,6 +31,11 @@ export const DEFAULT_ADDITIONAL_CONFIG = {
30
31
  active: true,
31
32
  },
32
33
  },
34
+ retry: {
35
+ attempts: 3,
36
+ initialDelayMs: 200,
37
+ backoff: 'exponential',
38
+ },
33
39
  };
34
40
  export class Apimo {
35
41
  constructor(
@@ -41,7 +47,23 @@ export class Apimo {
41
47
  config = DEFAULT_ADDITIONAL_CONFIG) {
42
48
  this.provider = provider;
43
49
  this.token = token;
50
+ if (!provider || provider.trim() === '') {
51
+ throw new ApiConfigurationError('provider must be a non-empty string.');
52
+ }
53
+ if (!token || token.trim() === '') {
54
+ throw new ApiConfigurationError('token must be a non-empty string.');
55
+ }
44
56
  this.config = merge(DEFAULT_ADDITIONAL_CONFIG, config);
57
+ if (!this.config.baseUrl || this.config.baseUrl.trim() === '') {
58
+ throw new ApiConfigurationError('baseUrl must be a non-empty string.');
59
+ }
60
+ try {
61
+ // eslint-disable-next-line no-new
62
+ new URL(this.config.baseUrl);
63
+ }
64
+ catch (_a) {
65
+ throw new ApiConfigurationError(`baseUrl "${this.config.baseUrl}" is not a valid URL.`);
66
+ }
45
67
  this.cache = this.config.catalogs.cache.active ? this.config.catalogs.cache.adapter : new DummyCache();
46
68
  this.limiter = new Bottleneck({
47
69
  reservoir: 10,
@@ -59,11 +81,43 @@ export class Apimo {
59
81
  }
60
82
  get(path, schema, options) {
61
83
  return __awaiter(this, void 0, void 0, function* () {
62
- const response = yield this.fetch(makeApiUrl(path, this.config, Object.assign({ culture: this.config.culture }, options)));
63
- if (!response.ok) {
64
- throw new Error(yield response.json());
84
+ const url = makeApiUrl(path, this.config, Object.assign({ culture: this.config.culture }, options));
85
+ const { attempts, initialDelayMs, backoff } = this.config.retry;
86
+ let lastError;
87
+ for (let attempt = 1; attempt <= attempts; attempt++) {
88
+ try {
89
+ const response = yield this.fetch(url);
90
+ if (!response.ok) {
91
+ let responseBody;
92
+ try {
93
+ responseBody = yield response.json();
94
+ }
95
+ catch (_a) {
96
+ // The body wasn't JSON — leave responseBody as undefined
97
+ }
98
+ throwForStatus(response.status, url.toString(), responseBody);
99
+ }
100
+ const json = yield response.json();
101
+ const result = yield schema.safeParseAsync(json);
102
+ if (!result.success) {
103
+ throw new ApiResponseValidationError(url.toString(), result.error);
104
+ }
105
+ return result.data;
106
+ }
107
+ catch (error) {
108
+ lastError = error;
109
+ const hasMoreAttempts = attempt < attempts;
110
+ if (!isRetryable(error)) {
111
+ // Non-transient errors (4xx, schema failures, etc.) — propagate immediately
112
+ throw error;
113
+ }
114
+ if (!hasMoreAttempts) {
115
+ break;
116
+ }
117
+ yield this.sleep(this.retryDelayMs(attempt, initialDelayMs, backoff));
118
+ }
65
119
  }
66
- return schema.parseAsync(yield response.json());
120
+ throw new ApiRetryExhaustedError(attempts, lastError);
67
121
  });
68
122
  }
69
123
  fetchCatalogs() {
@@ -128,6 +182,17 @@ export class Apimo {
128
182
  }), options);
129
183
  });
130
184
  }
185
+ /** Calculates the delay before the next retry attempt (1-based attempt index). */
186
+ retryDelayMs(attempt, initialDelayMs, backoff) {
187
+ switch (backoff) {
188
+ case 'exponential': return initialDelayMs * Math.pow(2, (attempt - 1));
189
+ case 'linear': return initialDelayMs * attempt;
190
+ case 'fixed': return initialDelayMs;
191
+ }
192
+ }
193
+ sleep(ms) {
194
+ return new Promise(resolve => setTimeout(resolve, ms));
195
+ }
131
196
  getLocalizedCatalogTransformer(culture) {
132
197
  return (catalogName, id) => __awaiter(this, void 0, void 0, function* () {
133
198
  if (!this.config.catalogs.transform.active) {